@salesforce/ui-bundle-template-feature-react-search 11.3.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 (169) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +692 -0
  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 +3499 -0
  8. package/dist/README.md +28 -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-react-search/.forceignore +15 -0
  12. package/dist/force-app/main/default/uiBundles/feature-react-search/.graphqlrc.yml +2 -0
  13. package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierignore +9 -0
  14. package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierrc +11 -0
  15. package/dist/force-app/main/default/uiBundles/feature-react-search/CHANGELOG.md +10 -0
  16. package/dist/force-app/main/default/uiBundles/feature-react-search/README.md +75 -0
  17. package/dist/force-app/main/default/uiBundles/feature-react-search/codegen.yml +95 -0
  18. package/dist/force-app/main/default/uiBundles/feature-react-search/components.json +18 -0
  19. package/dist/force-app/main/default/uiBundles/feature-react-search/e2e/app.spec.ts +17 -0
  20. package/dist/force-app/main/default/uiBundles/feature-react-search/eslint.config.js +169 -0
  21. package/dist/force-app/main/default/uiBundles/feature-react-search/feature-react-search.uibundle-meta.xml +7 -0
  22. package/dist/force-app/main/default/uiBundles/feature-react-search/index.html +12 -0
  23. package/dist/force-app/main/default/uiBundles/feature-react-search/package.json +76 -0
  24. package/dist/force-app/main/default/uiBundles/feature-react-search/playwright.config.ts +24 -0
  25. package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/get-graphql-schema.mjs +71 -0
  26. package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/rewrite-e2e-assets.mjs +23 -0
  27. package/dist/force-app/main/default/uiBundles/feature-react-search/src/api/graphqlClient.ts +44 -0
  28. package/dist/force-app/main/default/uiBundles/feature-react-search/src/app.tsx +17 -0
  29. package/dist/force-app/main/default/uiBundles/feature-react-search/src/appLayout.tsx +83 -0
  30. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/book.svg +3 -0
  31. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/copy.svg +4 -0
  32. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/rocket.svg +3 -0
  33. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/star.svg +3 -0
  34. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-1.png +0 -0
  35. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-2.png +0 -0
  36. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-3.png +0 -0
  37. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/vibe-codey.svg +194 -0
  38. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/alerts/status-alert.tsx +52 -0
  39. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/layouts/card-layout.tsx +29 -0
  40. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/alert.tsx +76 -0
  41. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/avatar.tsx +109 -0
  42. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/badge.tsx +48 -0
  43. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/breadcrumb.tsx +109 -0
  44. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/button.tsx +67 -0
  45. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/calendar.tsx +232 -0
  46. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/card.tsx +103 -0
  47. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/checkbox.tsx +32 -0
  48. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/collapsible.tsx +33 -0
  49. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/datePicker.tsx +127 -0
  50. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dialog.tsx +162 -0
  51. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dropdown-menu.tsx +257 -0
  52. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/field.tsx +237 -0
  53. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/index.ts +109 -0
  54. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/input.tsx +19 -0
  55. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/label.tsx +22 -0
  56. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/pagination.tsx +132 -0
  57. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/popover.tsx +89 -0
  58. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/select.tsx +193 -0
  59. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/separator.tsx +26 -0
  60. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/skeleton.tsx +14 -0
  61. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/sonner.tsx +20 -0
  62. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/spinner.tsx +16 -0
  63. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/table.tsx +114 -0
  64. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/tabs.tsx +88 -0
  65. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
  66. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
  67. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
  68. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
  69. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
  70. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
  71. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  72. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
  73. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
  74. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  75. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
  76. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
  77. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  78. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
  79. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  80. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
  81. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  82. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  83. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  84. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  85. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
  86. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
  87. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
  88. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
  89. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
  90. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
  91. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
  92. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
  93. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
  94. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
  95. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
  96. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
  97. package/dist/force-app/main/default/uiBundles/feature-react-search/src/hooks/useAsyncData.ts +67 -0
  98. package/dist/force-app/main/default/uiBundles/feature-react-search/src/lib/utils.ts +6 -0
  99. package/dist/force-app/main/default/uiBundles/feature-react-search/src/navigationMenu.tsx +80 -0
  100. package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
  101. package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/NotFound.tsx +18 -0
  102. package/dist/force-app/main/default/uiBundles/feature-react-search/src/router-utils.tsx +35 -0
  103. package/dist/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +22 -0
  104. package/dist/force-app/main/default/uiBundles/feature-react-search/src/styles/global.css +135 -0
  105. package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.json +45 -0
  106. package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.node.json +13 -0
  107. package/dist/force-app/main/default/uiBundles/feature-react-search/ui-bundle.json +7 -0
  108. package/dist/force-app/main/default/uiBundles/feature-react-search/vite-env.d.ts +4 -0
  109. package/dist/force-app/main/default/uiBundles/feature-react-search/vite.config.ts +106 -0
  110. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest-env.d.ts +2 -0
  111. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.config.ts +11 -0
  112. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.setup.ts +1 -0
  113. package/dist/jest.config.js +6 -0
  114. package/dist/package-lock.json +9995 -0
  115. package/dist/package.json +44 -0
  116. package/dist/scripts/apex/hello.apex +10 -0
  117. package/dist/scripts/gitignore-templates.json +4 -0
  118. package/dist/scripts/graphql-search.sh +191 -0
  119. package/dist/scripts/org-setup-config-schema.mjs +96 -0
  120. package/dist/scripts/org-setup.config.json +5 -0
  121. package/dist/scripts/org-setup.mjs +1392 -0
  122. package/dist/scripts/sf-project-setup.mjs +103 -0
  123. package/dist/scripts/soql/account.soql +6 -0
  124. package/dist/scripts/validate-org-setup-config.mjs +38 -0
  125. package/dist/sfdx-project.json +12 -0
  126. package/package.json +51 -0
  127. package/src/force-app/main/default/uiBundles/feature-react-search/src/__inherit__appLayout.tsx +9 -0
  128. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__alert.tsx +39 -0
  129. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__button.tsx +45 -0
  130. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__checkbox.tsx +8 -0
  131. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__input.tsx +5 -0
  132. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__label.tsx +8 -0
  133. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__pagination.tsx +47 -0
  134. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__select.tsx +57 -0
  135. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__skeleton.tsx +5 -0
  136. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
  137. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
  138. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
  139. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
  140. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
  141. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
  142. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  143. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
  144. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
  145. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  146. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
  147. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
  148. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  149. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
  150. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  151. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
  152. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  153. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  154. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  155. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  156. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
  157. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
  158. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
  159. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
  160. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
  161. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
  162. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
  163. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
  164. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
  165. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
  166. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
  167. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
  168. package/src/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
  169. package/src/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +10 -0
@@ -0,0 +1,61 @@
1
+ import { X } from "lucide-react";
2
+ import { Button } from "../../../../components/ui/__inherit__button";
3
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
4
+
5
+ function formatLabel(filter: ActiveFilterValue): string {
6
+ const { label, type, value, min, max } = filter;
7
+ switch (type) {
8
+ case "text":
9
+ case "picklist":
10
+ return `${label}: ${value ?? ""}`;
11
+ case "multipicklist": {
12
+ const values = value ? value.split(",") : [];
13
+ if (values.length <= 2) return `${label}: ${values.join(", ")}`;
14
+ return `${label}: ${values.length} selected`;
15
+ }
16
+ case "boolean":
17
+ return `${label}: ${value === "true" ? "Yes" : "No"}`;
18
+ case "numeric": {
19
+ if (min && max) return `${label}: ${min} – ${max}`;
20
+ if (min) return `${label}: ≥ ${min}`;
21
+ return `${label}: ≤ ${max ?? ""}`;
22
+ }
23
+ case "date":
24
+ case "daterange":
25
+ case "datetime":
26
+ case "datetimerange": {
27
+ if (min && max) return `${label}: ${min} → ${max}`;
28
+ if (min) return `${label}: from ${min}`;
29
+ return `${label}: until ${max ?? ""}`;
30
+ }
31
+ default:
32
+ return label;
33
+ }
34
+ }
35
+
36
+ interface ActiveFiltersProps {
37
+ filters: ActiveFilterValue[];
38
+ onRemove: (field: string) => void;
39
+ className?: string;
40
+ }
41
+
42
+ export function ActiveFilters({ filters, onRemove, className }: ActiveFiltersProps) {
43
+ if (filters.length === 0) return null;
44
+ return (
45
+ <div className={`flex flex-wrap gap-2 ${className ?? ""}`}>
46
+ {filters.map((filter) => (
47
+ <Button
48
+ key={filter.field}
49
+ variant="outline"
50
+ size="sm"
51
+ className="gap-1 h-7 text-xs"
52
+ aria-label={`Remove ${formatLabel(filter)}`}
53
+ onClick={() => onRemove(filter.field)}
54
+ >
55
+ {formatLabel(filter)}
56
+ <X className="h-3 w-3" aria-hidden="true" />
57
+ </Button>
58
+ ))}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Default filter panel used when no `renderFilters` is supplied for a source.
3
+ *
4
+ * Renders one input per entry in `source.filterFields`, picking the right
5
+ * component for each `FilterFieldType`:
6
+ *
7
+ * - text → TextFilter
8
+ * - picklist → SelectFilter (inline options or auto-fetched)
9
+ * - multipicklist → MultiSelectFilter (inline options or auto-fetched)
10
+ * - numeric → NumericRangeFilter
11
+ * - boolean → BooleanFilter
12
+ * - date / daterange / datetime / datetimerange → DateRangeFilter
13
+ *
14
+ * Applications can still pass `renderFilters[sourceKey]` to override the
15
+ * entire panel for a specific source.
16
+ */
17
+
18
+ import { TextFilter } from "./inputs/TextFilter";
19
+ import { SelectFilter } from "./inputs/SelectFilter";
20
+ import { MultiSelectFilter } from "./inputs/MultiSelectFilter";
21
+ import { NumericRangeFilter } from "./inputs/NumericRangeFilter";
22
+ import { BooleanFilter } from "./inputs/BooleanFilter";
23
+ import { DateRangeFilter } from "./inputs/DateRangeFilter";
24
+ import { useDistinctValues } from "../../hooks/useDistinctValues";
25
+ import type { FilterFieldConfig } from "../../utils/filterUtils";
26
+ import type { SObjectSourceConfig } from "../../types";
27
+
28
+ interface DefaultFilterPanelProps {
29
+ source: SObjectSourceConfig;
30
+ }
31
+
32
+ export function DefaultFilterPanel({ source }: DefaultFilterPanelProps) {
33
+ const filters = source.filterFields ?? [];
34
+ if (filters.length === 0) return null;
35
+ return (
36
+ <>
37
+ {filters.map((filter) => (
38
+ <DefaultFilterField key={filter.field} filter={filter} source={source} />
39
+ ))}
40
+ </>
41
+ );
42
+ }
43
+
44
+ interface DefaultFilterFieldProps {
45
+ filter: FilterFieldConfig;
46
+ source: SObjectSourceConfig;
47
+ }
48
+
49
+ function DefaultFilterField({ filter, source }: DefaultFilterFieldProps) {
50
+ switch (filter.type) {
51
+ case "text":
52
+ return (
53
+ <TextFilter
54
+ field={filter.field}
55
+ label={filter.label}
56
+ placeholder={filter.placeholder}
57
+ helpText={filter.helpText}
58
+ />
59
+ );
60
+ case "picklist":
61
+ return <PicklistField filter={filter} source={source} multi={false} />;
62
+ case "multipicklist":
63
+ return <PicklistField filter={filter} source={source} multi={true} />;
64
+ case "numeric":
65
+ return (
66
+ <NumericRangeFilter field={filter.field} label={filter.label} helpText={filter.helpText} />
67
+ );
68
+ case "boolean":
69
+ return <BooleanFilter field={filter.field} label={filter.label} helpText={filter.helpText} />;
70
+ case "date":
71
+ case "daterange":
72
+ return (
73
+ <DateRangeFilter
74
+ field={filter.field}
75
+ label={filter.label}
76
+ helpText={filter.helpText}
77
+ filterType="daterange"
78
+ />
79
+ );
80
+ case "datetime":
81
+ case "datetimerange":
82
+ return (
83
+ <DateRangeFilter
84
+ field={filter.field}
85
+ label={filter.label}
86
+ helpText={filter.helpText}
87
+ filterType="datetimerange"
88
+ />
89
+ );
90
+ default:
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Resolves picklist options from inline config first; falls back to fetching
97
+ * distinct values from the GraphQL aggregate API on first render.
98
+ */
99
+ function PicklistField({
100
+ filter,
101
+ source,
102
+ multi,
103
+ }: {
104
+ filter: FilterFieldConfig;
105
+ source: SObjectSourceConfig;
106
+ multi: boolean;
107
+ }) {
108
+ const inline = filter.options;
109
+ const fetched = useDistinctValues(
110
+ inline ? null : { objectName: source.objectName, fieldName: filter.field },
111
+ );
112
+ const options = inline ?? fetched ?? [];
113
+ const Component = multi ? MultiSelectFilter : SelectFilter;
114
+ return (
115
+ <Component
116
+ field={filter.field}
117
+ label={filter.label}
118
+ options={options}
119
+ helpText={filter.helpText}
120
+ />
121
+ );
122
+ }
@@ -0,0 +1,70 @@
1
+ import { createContext, useCallback, useContext, type ReactNode } from "react";
2
+ import { Button } from "../../../../components/ui/__inherit__button";
3
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
4
+
5
+ interface FilterContextValue {
6
+ filters: ActiveFilterValue[];
7
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
8
+ onFilterRemove: (field: string) => void;
9
+ onReset: () => void;
10
+ }
11
+
12
+ const FilterContext = createContext<FilterContextValue | null>(null);
13
+
14
+ interface FilterProviderProps {
15
+ filters: ActiveFilterValue[];
16
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
17
+ onFilterRemove: (field: string) => void;
18
+ onReset: () => void;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export function FilterProvider({
23
+ filters,
24
+ onFilterChange,
25
+ onFilterRemove,
26
+ onReset,
27
+ children,
28
+ }: FilterProviderProps) {
29
+ return (
30
+ <FilterContext.Provider value={{ filters, onFilterChange, onFilterRemove, onReset }}>
31
+ {children}
32
+ </FilterContext.Provider>
33
+ );
34
+ }
35
+
36
+ function useFilterContext() {
37
+ const ctx = useContext(FilterContext);
38
+ if (!ctx) throw new Error("useFilterField must be used within a FilterProvider");
39
+ return ctx;
40
+ }
41
+
42
+ export function useFilterField(field: string) {
43
+ const { filters, onFilterChange, onFilterRemove } = useFilterContext();
44
+ const value = filters.find((f) => f.field === field);
45
+ const onChange = useCallback(
46
+ (next: ActiveFilterValue | undefined) => {
47
+ if (next) onFilterChange(field, next);
48
+ else onFilterRemove(field);
49
+ },
50
+ [field, onFilterChange, onFilterRemove],
51
+ );
52
+ return { value, onChange };
53
+ }
54
+
55
+ export function useFilterPanel() {
56
+ const { filters, onReset } = useFilterContext();
57
+ return { hasActiveFilters: filters.length > 0, resetAll: onReset };
58
+ }
59
+
60
+ type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
61
+
62
+ export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
63
+ const { hasActiveFilters, resetAll } = useFilterPanel();
64
+ if (!hasActiveFilters) return null;
65
+ return (
66
+ <Button onClick={resetAll} aria-label="Reset filters" variant="destructive" {...props}>
67
+ {children ?? "Reset"}
68
+ </Button>
69
+ );
70
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "../../../../../components/ui/__inherit__select";
8
+ import { useFilterField } from "../FilterContext";
9
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
10
+
11
+ const ANY_VALUE = "__any__";
12
+
13
+ interface BooleanFilterProps {
14
+ field: string;
15
+ label: string;
16
+ trueLabel?: string;
17
+ falseLabel?: string;
18
+ helpText?: string;
19
+ }
20
+
21
+ export function BooleanFilter({
22
+ field,
23
+ label,
24
+ trueLabel = "Yes",
25
+ falseLabel = "No",
26
+ helpText,
27
+ }: BooleanFilterProps) {
28
+ const { value, onChange } = useFilterField(field);
29
+ const id = `filter-${field}`;
30
+ return (
31
+ <FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
32
+ <Select
33
+ value={value?.value ?? ANY_VALUE}
34
+ onValueChange={(v) => {
35
+ if (v === ANY_VALUE) onChange(undefined);
36
+ else onChange({ field, label, type: "boolean", value: v });
37
+ }}
38
+ >
39
+ <SelectTrigger id={id} className="w-full">
40
+ <SelectValue />
41
+ </SelectTrigger>
42
+ <SelectContent>
43
+ <SelectItem value={ANY_VALUE}>Any</SelectItem>
44
+ <SelectItem value="true">{trueLabel}</SelectItem>
45
+ <SelectItem value="false">{falseLabel}</SelectItem>
46
+ </SelectContent>
47
+ </Select>
48
+ </FilterFieldWrapper>
49
+ );
50
+ }
@@ -0,0 +1,50 @@
1
+ import { Input } from "../../../../../components/ui/__inherit__input";
2
+ import { useFilterField } from "../FilterContext";
3
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
4
+ import type { FilterFieldType } from "../../../utils/filterUtils";
5
+
6
+ interface DateRangeFilterProps {
7
+ field: string;
8
+ label: string;
9
+ helpText?: string;
10
+ /** "daterange" (Date scalar) or "datetimerange" (DateTime scalar). */
11
+ filterType?: Extract<FilterFieldType, "daterange" | "datetimerange">;
12
+ }
13
+
14
+ /**
15
+ * Two `<input type="date">` controls bound to `min` / `max` of the active
16
+ * filter. Uses the platform-native picker — no popover/calendar dependency.
17
+ */
18
+ export function DateRangeFilter({
19
+ field,
20
+ label,
21
+ helpText,
22
+ filterType = "daterange",
23
+ }: DateRangeFilterProps) {
24
+ const { value, onChange } = useFilterField(field);
25
+
26
+ const update = (nextMin: string | undefined, nextMax: string | undefined) => {
27
+ if (!nextMin && !nextMax) onChange(undefined);
28
+ else onChange({ field, label, type: filterType, min: nextMin, max: nextMax });
29
+ };
30
+
31
+ return (
32
+ <FilterFieldWrapper label={label} helpText={helpText}>
33
+ <div className="flex items-center gap-2">
34
+ <Input
35
+ type="date"
36
+ value={value?.min ?? ""}
37
+ onChange={(e) => update(e.target.value || undefined, value?.max)}
38
+ aria-label={`${label} from`}
39
+ />
40
+ <span className="text-sm text-muted-foreground">–</span>
41
+ <Input
42
+ type="date"
43
+ value={value?.max ?? ""}
44
+ onChange={(e) => update(value?.min, e.target.value || undefined)}
45
+ aria-label={`${label} to`}
46
+ />
47
+ </div>
48
+ </FilterFieldWrapper>
49
+ );
50
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ import { Label } from "../../../../../components/ui/__inherit__label";
3
+
4
+ interface FilterFieldWrapperProps {
5
+ label: string;
6
+ htmlFor?: string;
7
+ helpText?: string;
8
+ className?: string;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function FilterFieldWrapper({
13
+ label,
14
+ htmlFor,
15
+ helpText,
16
+ className,
17
+ children,
18
+ }: FilterFieldWrapperProps) {
19
+ return (
20
+ <div className={`space-y-1 ${className ?? ""}`}>
21
+ <Label htmlFor={htmlFor}>{label}</Label>
22
+ {children}
23
+ {helpText && <p className="text-xs text-muted-foreground min-h-4">{helpText}</p>}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,47 @@
1
+ import { Checkbox } from "../../../../../components/ui/__inherit__checkbox";
2
+ import { Label } from "../../../../../components/ui/__inherit__label";
3
+ import { useFilterField } from "../FilterContext";
4
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
5
+
6
+ interface MultiSelectFilterProps {
7
+ field: string;
8
+ label: string;
9
+ options: Array<{ value: string; label: string }>;
10
+ helpText?: string;
11
+ }
12
+
13
+ export function MultiSelectFilter({ field, label, options, helpText }: MultiSelectFilterProps) {
14
+ const { value, onChange } = useFilterField(field);
15
+ const selected = new Set(value?.value ? value.value.split(",").filter(Boolean) : []);
16
+
17
+ const toggle = (v: string, on: boolean) => {
18
+ const next = new Set(selected);
19
+ if (on) next.add(v);
20
+ else next.delete(v);
21
+ const joined = Array.from(next).join(",");
22
+ if (joined === "") onChange(undefined);
23
+ else onChange({ field, label, type: "multipicklist", value: joined });
24
+ };
25
+
26
+ return (
27
+ <FilterFieldWrapper label={label} helpText={helpText}>
28
+ <div className="flex flex-col gap-1">
29
+ {options.map((opt) => {
30
+ const id = `filter-${field}-${opt.value}`;
31
+ return (
32
+ <div key={opt.value} className="flex items-center gap-2">
33
+ <Checkbox
34
+ id={id}
35
+ checked={selected.has(opt.value)}
36
+ onCheckedChange={(checked) => toggle(opt.value, checked === true)}
37
+ />
38
+ <Label htmlFor={id} className="text-sm font-normal cursor-pointer">
39
+ {opt.label}
40
+ </Label>
41
+ </div>
42
+ );
43
+ })}
44
+ </div>
45
+ </FilterFieldWrapper>
46
+ );
47
+ }
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Input } from "../../../../../components/ui/__inherit__input";
3
+ import { useFilterField } from "../FilterContext";
4
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
5
+ import { debounce, FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
6
+
7
+ interface NumericRangeFilterProps {
8
+ field: string;
9
+ label: string;
10
+ helpText?: string;
11
+ min?: number;
12
+ max?: number;
13
+ }
14
+
15
+ export function NumericRangeFilter({ field, label, helpText, min, max }: NumericRangeFilterProps) {
16
+ const { value, onChange } = useFilterField(field);
17
+ const [draftMin, setDraftMin] = useState(value?.min ?? "");
18
+ const [draftMax, setDraftMax] = useState(value?.max ?? "");
19
+
20
+ const onChangeRef = useRef(onChange);
21
+ useEffect(() => {
22
+ onChangeRef.current = onChange;
23
+ });
24
+ // Build the debounced caller in a mount effect, not during render: creating it
25
+ // inline would read onChangeRef.current at render time (react-hooks/refs). It
26
+ // stays a single stable instance and always calls the latest onChange via the ref.
27
+ const debouncedRef = useRef<((nextMin: string, nextMax: string) => void) | undefined>(undefined);
28
+ useEffect(() => {
29
+ debouncedRef.current = debounce((nextMin: string, nextMax: string) => {
30
+ if (!nextMin && !nextMax) onChangeRef.current(undefined);
31
+ else
32
+ onChangeRef.current({
33
+ field,
34
+ label,
35
+ type: "numeric",
36
+ min: nextMin || undefined,
37
+ max: nextMax || undefined,
38
+ });
39
+ }, FILTER_DEBOUNCE_MS);
40
+ }, [field, label]);
41
+
42
+ useEffect(() => {
43
+ setDraftMin(value?.min ?? "");
44
+ setDraftMax(value?.max ?? "");
45
+ }, [value?.min, value?.max]);
46
+
47
+ return (
48
+ <FilterFieldWrapper label={label} helpText={helpText}>
49
+ <div className="flex items-center gap-2">
50
+ <Input
51
+ type="number"
52
+ placeholder="Min"
53
+ min={min}
54
+ max={max}
55
+ value={draftMin}
56
+ onChange={(e) => {
57
+ setDraftMin(e.target.value);
58
+ debouncedRef.current?.(e.target.value, draftMax);
59
+ }}
60
+ aria-label={`${label} minimum`}
61
+ />
62
+ <span className="text-sm text-muted-foreground">–</span>
63
+ <Input
64
+ type="number"
65
+ placeholder="Max"
66
+ min={min}
67
+ max={max}
68
+ value={draftMax}
69
+ onChange={(e) => {
70
+ setDraftMax(e.target.value);
71
+ debouncedRef.current?.(draftMin, e.target.value);
72
+ }}
73
+ aria-label={`${label} maximum`}
74
+ />
75
+ </div>
76
+ </FilterFieldWrapper>
77
+ );
78
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "../../../../../components/ui/__inherit__select";
8
+ import { useFilterField } from "../FilterContext";
9
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
10
+
11
+ const ALL_VALUE = "__all__";
12
+
13
+ interface SelectFilterProps {
14
+ field: string;
15
+ label: string;
16
+ options: Array<{ value: string; label: string }>;
17
+ helpText?: string;
18
+ }
19
+
20
+ export function SelectFilter({ field, label, options, helpText }: SelectFilterProps) {
21
+ const { value, onChange } = useFilterField(field);
22
+ const id = `filter-${field}`;
23
+ return (
24
+ <FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
25
+ <Select
26
+ value={value?.value ?? ALL_VALUE}
27
+ onValueChange={(v) => {
28
+ if (v === ALL_VALUE) onChange(undefined);
29
+ else onChange({ field, label, type: "picklist", value: v });
30
+ }}
31
+ >
32
+ <SelectTrigger id={id} className="w-full">
33
+ <SelectValue placeholder={`Select ${label.toLowerCase()}`} />
34
+ </SelectTrigger>
35
+ <SelectContent>
36
+ <SelectItem value={ALL_VALUE}>All</SelectItem>
37
+ {options.map((opt) => (
38
+ <SelectItem key={opt.value} value={opt.value}>
39
+ {opt.label}
40
+ </SelectItem>
41
+ ))}
42
+ </SelectContent>
43
+ </Select>
44
+ </FilterFieldWrapper>
45
+ );
46
+ }
@@ -0,0 +1,52 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Input } from "../../../../../components/ui/__inherit__input";
3
+ import { useFilterField } from "../FilterContext";
4
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
5
+ import { debounce, FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
6
+
7
+ interface TextFilterProps {
8
+ field: string;
9
+ label: string;
10
+ placeholder?: string;
11
+ helpText?: string;
12
+ }
13
+
14
+ export function TextFilter({ field, label, placeholder, helpText }: TextFilterProps) {
15
+ const { value, onChange } = useFilterField(field);
16
+ const [draft, setDraft] = useState(value?.value ?? "");
17
+
18
+ const onChangeRef = useRef(onChange);
19
+ useEffect(() => {
20
+ onChangeRef.current = onChange;
21
+ });
22
+ // Build the debounced caller in a mount effect, not during render: creating it
23
+ // inline would read onChangeRef.current at render time (react-hooks/refs). It
24
+ // stays a single stable instance and always calls the latest onChange via the ref.
25
+ const debouncedRef = useRef<((next: string) => void) | undefined>(undefined);
26
+ useEffect(() => {
27
+ debouncedRef.current = debounce((next: string) => {
28
+ if (next === "") onChangeRef.current(undefined);
29
+ else onChangeRef.current({ field, label, type: "text", value: next });
30
+ }, FILTER_DEBOUNCE_MS);
31
+ }, [field, label]);
32
+
33
+ useEffect(() => {
34
+ setDraft(value?.value ?? "");
35
+ }, [value?.value]);
36
+
37
+ const id = `filter-${field}`;
38
+ return (
39
+ <FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
40
+ <Input
41
+ id={id}
42
+ type="text"
43
+ value={draft}
44
+ placeholder={placeholder}
45
+ onChange={(e) => {
46
+ setDraft(e.target.value);
47
+ debouncedRef.current?.(e.target.value);
48
+ }}
49
+ />
50
+ </FilterFieldWrapper>
51
+ );
52
+ }