@salesforce/webapp-template-app-react-sample-b2x-experimental 1.112.7 → 1.112.9

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 (138) hide show
  1. package/dist/CHANGELOG.md +19 -0
  2. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +6 -5
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +12058 -214
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +18 -15
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyListingGraphQL.ts → properties/propertyListingGraphQL.ts} +1 -1
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +4 -2
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{TopBar.tsx → layout/TopBar.tsx} +2 -2
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestList.tsx → maintenanceRequests/MaintenanceRequestList.tsx} +4 -4
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestListItem.tsx → maintenanceRequests/MaintenanceRequestListItem.tsx} +3 -3
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +87 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyListingCard.tsx → properties/PropertyListingCard.tsx} +1 -1
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/components/search/SearchPagination.tsx → components/properties/PropertyListingSearchPagination.tsx} +20 -28
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants/propertyListing.ts +4 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +303 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/FilterContext.tsx +73 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/PaginationControls.tsx +109 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SortControl.tsx +143 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/BooleanFilter.tsx +74 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateFilter.tsx +121 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +69 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +98 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +85 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SearchFilter.tsx +37 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SelectFilter.tsx +93 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/TextFilter.tsx +74 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useAsyncData.ts +54 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useCachedAsyncData.ts +184 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useObjectSearchParams.ts +247 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/debounce.ts +22 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +1 -1
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +1 -1
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +2 -2
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +2 -2
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +3 -3
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +2 -2
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +3 -3
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +2 -2
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +4 -2
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +2 -2
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +1 -1
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +12 -10
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +6 -18
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/propertyListingPaginationUtils.ts +18 -0
  59. package/dist/package-lock.json +2 -2
  60. package/dist/package.json +1 -1
  61. package/dist/scripts/graphql-search.sh +69 -17
  62. package/package.json +1 -1
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceDetailsModal.tsx +0 -128
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/constants.ts +0 -39
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/form.tsx +0 -209
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/filters.ts +0 -121
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/schema.d.ts +0 -200
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/debounce.ts +0 -90
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formUtils.ts +0 -142
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +0 -64
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/index.ts +0 -120
  126. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/About.tsx +0 -8
  127. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +0 -29
  128. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +0 -100
  129. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{applicationApi.ts → applications/applicationApi.ts} +0 -0
  130. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{maintenanceRequestApi.ts → maintenanceRequests/maintenanceRequestApi.ts} +0 -0
  131. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyDetailGraphQL.ts → properties/propertyDetailGraphQL.ts} +0 -0
  132. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{WeatherWidget.tsx → dashboard/WeatherWidget.tsx} +0 -0
  133. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{NavMenu.tsx → layout/VerticalNav.tsx} +0 -0
  134. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestIcon.tsx → maintenanceRequests/MaintenanceRequestIcon.tsx} +0 -0
  135. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{StatusBadge.tsx → maintenanceRequests/StatusBadge.tsx} +0 -0
  136. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyMap.tsx → properties/PropertyMap.tsx} +0 -0
  137. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertySearchFilters.tsx → properties/PropertySearchFilters.tsx} +0 -0
  138. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/types/search → types}/searchResults.ts +0 -0
@@ -0,0 +1,121 @@
1
+ import { useState } from "react";
2
+ import { parseISO } from "date-fns";
3
+ import { Label } from "../../../../components/ui/label";
4
+ import {
5
+ DatePicker,
6
+ DatePickerTrigger,
7
+ DatePickerContent,
8
+ DatePickerCalendar,
9
+ } from "../../../../components/ui/datePicker";
10
+ import { cn } from "../../../../lib/utils";
11
+ import { useFilterField } from "../FilterContext";
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "../../../../components/ui/select";
19
+
20
+ type DateOperator = "gt" | "lt";
21
+
22
+ const OPERATOR_OPTIONS: { value: DateOperator; label: string }[] = [
23
+ { value: "gt", label: "After" },
24
+ { value: "lt", label: "Before" },
25
+ ];
26
+
27
+ function operatorToField(op: DateOperator): "min" | "max" {
28
+ return op === "gt" ? "min" : "max";
29
+ }
30
+
31
+ interface DateFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
32
+ field: string;
33
+ label: string;
34
+ helpText?: string;
35
+ }
36
+
37
+ export function DateFilter({ field, label, helpText, className, ...props }: DateFilterProps) {
38
+ const { value, onChange } = useFilterField(field);
39
+
40
+ const initialOp: DateOperator = value?.min ? "gt" : "lt";
41
+ const [operator, setOperator] = useState<DateOperator>(initialOp);
42
+
43
+ const currentDate = toDate(value?.min ?? value?.max);
44
+
45
+ function handleOperatorChange(op: DateOperator) {
46
+ setOperator(op);
47
+ if (currentDate) {
48
+ emitChange(op, currentDate);
49
+ }
50
+ }
51
+
52
+ function handleDateChange(date: Date | undefined) {
53
+ if (!date) {
54
+ onChange(undefined);
55
+ } else {
56
+ emitChange(operator, date);
57
+ }
58
+ }
59
+
60
+ function emitChange(op: DateOperator, date: Date) {
61
+ const dateStr = toDateString(date);
62
+ const f = operatorToField(op);
63
+ onChange({
64
+ field,
65
+ label,
66
+ type: "date",
67
+ value: op,
68
+ min: f === "min" ? dateStr : undefined,
69
+ max: f === "max" ? dateStr : undefined,
70
+ });
71
+ }
72
+
73
+ return (
74
+ <div className={cn("space-y-1.5", className)} {...props}>
75
+ <Label>{label}</Label>
76
+ <div className="flex gap-2">
77
+ <Select value={operator} onValueChange={(v) => handleOperatorChange(v as DateOperator)}>
78
+ <SelectTrigger className="w-full flex-1">
79
+ <SelectValue />
80
+ </SelectTrigger>
81
+ <SelectContent>
82
+ {OPERATOR_OPTIONS.map((opt) => (
83
+ <SelectItem key={opt.value} value={opt.value}>
84
+ {opt.label}
85
+ </SelectItem>
86
+ ))}
87
+ </SelectContent>
88
+ </Select>
89
+ <DatePicker>
90
+ <DatePickerTrigger
91
+ className="w-full flex-2"
92
+ date={currentDate}
93
+ dateFormat="MMM do, yyyy"
94
+ placeholder="Pick a date"
95
+ aria-label={label}
96
+ />
97
+ <DatePickerContent>
98
+ <DatePickerCalendar
99
+ mode="single"
100
+ captionLayout="dropdown"
101
+ selected={currentDate}
102
+ onSelect={handleDateChange}
103
+ />
104
+ </DatePickerContent>
105
+ </DatePicker>
106
+ </div>
107
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ export function toDate(value: string | undefined): Date | undefined {
113
+ if (!value) return undefined;
114
+ const parsed = parseISO(value);
115
+ return isNaN(parsed.getTime()) ? undefined : parsed;
116
+ }
117
+
118
+ export function toDateString(date: Date | undefined): string {
119
+ if (!date) return "";
120
+ return date.toISOString().split("T")[0];
121
+ }
@@ -0,0 +1,69 @@
1
+ import type { DateRange } from "react-day-picker";
2
+ import { Label } from "../../../../components/ui/label";
3
+ import {
4
+ DatePicker,
5
+ DatePickerRangeTrigger,
6
+ DatePickerContent,
7
+ DatePickerCalendar,
8
+ } from "../../../../components/ui/datePicker";
9
+ import { cn } from "../../../../lib/utils";
10
+ import { useFilterField } from "../FilterContext";
11
+ import { toDate, toDateString } from "./DateFilter";
12
+
13
+ interface DateRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
14
+ field: string;
15
+ label: string;
16
+ helpText?: string;
17
+ }
18
+
19
+ export function DateRangeFilter({
20
+ field,
21
+ label,
22
+ helpText,
23
+ className,
24
+ ...props
25
+ }: DateRangeFilterProps) {
26
+ const { value, onChange } = useFilterField(field);
27
+
28
+ const dateRange: DateRange | undefined =
29
+ value?.min || value?.max ? { from: toDate(value?.min), to: toDate(value?.max) } : undefined;
30
+
31
+ function handleRangeSelect(range: DateRange | undefined) {
32
+ if (!range?.from && !range?.to) {
33
+ onChange(undefined);
34
+ } else {
35
+ onChange({
36
+ field,
37
+ label,
38
+ type: "daterange",
39
+ min: toDateString(range?.from),
40
+ max: toDateString(range?.to),
41
+ });
42
+ }
43
+ }
44
+
45
+ return (
46
+ <div className={cn("space-y-1.5", className)} {...props}>
47
+ <Label>{label}</Label>
48
+ <DatePicker>
49
+ <DatePickerRangeTrigger
50
+ className="w-full"
51
+ dateRange={dateRange}
52
+ placeholder="Pick a date range"
53
+ aria-label={label}
54
+ />
55
+ <DatePickerContent align="start">
56
+ <DatePickerCalendar
57
+ mode="range"
58
+ captionLayout="dropdown"
59
+ defaultMonth={dateRange?.from}
60
+ selected={dateRange}
61
+ onSelect={handleRangeSelect}
62
+ numberOfMonths={2}
63
+ />
64
+ </DatePickerContent>
65
+ </DatePicker>
66
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ Popover,
3
+ PopoverContent,
4
+ PopoverTrigger,
5
+ } from "../../../../components/ui/popover";
6
+ import { Checkbox } from "../../../../components/ui/checkbox";
7
+ import { Label } from "../../../../components/ui/label";
8
+ import { Button } from "../../../../components/ui/button";
9
+ import { cn } from "../../../../lib/utils";
10
+ import { ChevronDown } from "lucide-react";
11
+ import { useFilterField } from "../FilterContext";
12
+
13
+ interface MultiSelectFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
14
+ field: string;
15
+ label: string;
16
+ options: Array<{ value: string; label: string }>;
17
+ helpText?: string;
18
+ }
19
+
20
+ export function MultiSelectFilter({
21
+ field,
22
+ label,
23
+ options,
24
+ helpText,
25
+ className,
26
+ ...props
27
+ }: MultiSelectFilterProps) {
28
+ const { value, onChange } = useFilterField(field);
29
+ const selected = value?.value ? value.value.split(",") : [];
30
+
31
+ const triggerLabel =
32
+ selected.length === 0
33
+ ? `Select ${label.toLowerCase()}`
34
+ : selected.length === 1
35
+ ? (options.find((o) => o.value === selected[0])?.label ?? selected[0])
36
+ : `${selected.length} selected`;
37
+
38
+ function handleToggle(optionValue: string) {
39
+ const next = selected.includes(optionValue)
40
+ ? selected.filter((v) => v !== optionValue)
41
+ : [...selected, optionValue];
42
+
43
+ if (next.length === 0) {
44
+ onChange(undefined);
45
+ } else {
46
+ onChange({
47
+ field,
48
+ label,
49
+ type: "multipicklist",
50
+ value: next.join(","),
51
+ });
52
+ }
53
+ }
54
+
55
+ return (
56
+ <div className={cn("space-y-1.5", className)} {...props}>
57
+ <Label>{label}</Label>
58
+ <Popover>
59
+ <PopoverTrigger asChild>
60
+ <Button
61
+ variant="outline"
62
+ role="combobox"
63
+ className={cn(
64
+ "w-full justify-between font-normal",
65
+ selected.length === 0 && "text-muted-foreground",
66
+ )}
67
+ >
68
+ <span className="truncate">{triggerLabel}</span>
69
+ <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
70
+ </Button>
71
+ </PopoverTrigger>
72
+ <PopoverContent className="p-2" align="start">
73
+ <div className="max-h-48 overflow-y-auto space-y-1">
74
+ {options.map((opt) => {
75
+ const id = `filter-${field}-${opt.value}`;
76
+ return (
77
+ <div
78
+ key={opt.value}
79
+ className="flex items-center gap-2 rounded px-1 py-0.5 hover:bg-accent"
80
+ >
81
+ <Checkbox
82
+ id={id}
83
+ checked={selected.includes(opt.value)}
84
+ onCheckedChange={() => handleToggle(opt.value)}
85
+ />
86
+ <Label htmlFor={id} className="text-sm font-normal cursor-pointer w-full">
87
+ {opt.label}
88
+ </Label>
89
+ </div>
90
+ );
91
+ })}
92
+ </div>
93
+ </PopoverContent>
94
+ </Popover>
95
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,85 @@
1
+ import { Input } from "../../../../components/ui/input";
2
+ import { Label } from "../../../../components/ui/label";
3
+ import { cn } from "../../../../lib/utils";
4
+ import { useFilterField } from "../FilterContext";
5
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
6
+
7
+ interface NumericRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
8
+ field: string;
9
+ label: string;
10
+ helpText?: string;
11
+ }
12
+
13
+ export function NumericRangeFilter({
14
+ field,
15
+ label,
16
+ helpText,
17
+ className,
18
+ ...props
19
+ }: NumericRangeFilterProps) {
20
+ const { value, onChange } = useFilterField(field);
21
+ return (
22
+ <div className={cn("space-y-1.5", className)} {...props}>
23
+ <Label>{label}</Label>
24
+ <NumericRangeFilterInputs field={field} label={label} value={value} onChange={onChange} />
25
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ interface NumericRangeFilterInputsProps extends Omit<React.ComponentProps<"div">, "onChange"> {
31
+ field: string;
32
+ label: string;
33
+ value: ActiveFilterValue | undefined;
34
+ onChange: (value: ActiveFilterValue | undefined) => void;
35
+ minInputProps?: React.ComponentProps<typeof Input>;
36
+ maxInputProps?: React.ComponentProps<typeof Input>;
37
+ }
38
+
39
+ export function NumericRangeFilterInputs({
40
+ field,
41
+ label,
42
+ value,
43
+ onChange,
44
+ className,
45
+ minInputProps,
46
+ maxInputProps,
47
+ ...props
48
+ }: NumericRangeFilterInputsProps) {
49
+ const handleChange = (bound: "min" | "max", v: string) => {
50
+ const next = {
51
+ field,
52
+ label,
53
+ type: "numeric" as const,
54
+ min: value?.min ?? "",
55
+ max: value?.max ?? "",
56
+ [bound]: v,
57
+ };
58
+ if (!next.min && !next.max) {
59
+ onChange(undefined);
60
+ } else {
61
+ onChange(next);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <div className={cn("flex gap-2", className)} {...props}>
67
+ <Input
68
+ type="number"
69
+ placeholder="Min"
70
+ value={value?.min ?? ""}
71
+ onChange={(e) => handleChange("min", e.target.value)}
72
+ aria-label={`${label} minimum`}
73
+ {...minInputProps}
74
+ />
75
+ <Input
76
+ type="number"
77
+ placeholder="Max"
78
+ value={value?.max ?? ""}
79
+ onChange={(e) => handleChange("max", e.target.value)}
80
+ aria-label={`${label} maximum`}
81
+ {...maxInputProps}
82
+ />
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,37 @@
1
+ import { Label } from "../../../../components/ui/label";
2
+ import { cn } from "../../../../lib/utils";
3
+ import { SearchBar } from "../SearchBar";
4
+ import { useFilterField } from "../FilterContext";
5
+
6
+ interface SearchFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
7
+ field: string;
8
+ label: string;
9
+ placeholder?: string;
10
+ }
11
+
12
+ export function SearchFilter({
13
+ field,
14
+ label,
15
+ placeholder,
16
+ className,
17
+ ...props
18
+ }: SearchFilterProps) {
19
+ const { value, onChange } = useFilterField(field);
20
+ return (
21
+ <div className={cn("space-y-1.5", className)} {...props}>
22
+ <Label htmlFor={`filter-${field}`}>{label}</Label>
23
+ <SearchBar
24
+ value={value?.value ?? ""}
25
+ handleChange={(v) => {
26
+ if (v) {
27
+ onChange({ field, label, type: "search", value: v });
28
+ } else {
29
+ onChange(undefined);
30
+ }
31
+ }}
32
+ placeholder={placeholder}
33
+ inputProps={{ id: `filter-${field}` }}
34
+ />
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "../../../../components/ui/select";
8
+ import { Label } from "../../../../components/ui/label";
9
+ import { cn } from "../../../../lib/utils";
10
+ import { useFilterField } from "../FilterContext";
11
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
12
+
13
+ const ALL_VALUE = "__all__";
14
+
15
+ interface SelectFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
16
+ field: string;
17
+ label: string;
18
+ options: Array<{ value: string; label: string }>;
19
+ helpText?: string;
20
+ }
21
+
22
+ export function SelectFilter({
23
+ field,
24
+ label,
25
+ options,
26
+ helpText,
27
+ className,
28
+ ...props
29
+ }: SelectFilterProps) {
30
+ const { value, onChange } = useFilterField(field);
31
+ return (
32
+ <div className={cn("space-y-1.5", className)} {...props}>
33
+ <Label htmlFor={`filter-${field}`}>{label}</Label>
34
+ <SelectFilterControl
35
+ field={field}
36
+ label={label}
37
+ options={options}
38
+ value={value}
39
+ onChange={onChange}
40
+ />
41
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ interface SelectFilterControlProps {
47
+ field: string;
48
+ label: string;
49
+ options: Array<{ value: string; label: string }>;
50
+ value: ActiveFilterValue | undefined;
51
+ onChange: (value: ActiveFilterValue | undefined) => void;
52
+ triggerProps?: React.ComponentProps<typeof SelectTrigger>;
53
+ contentProps?: React.ComponentProps<typeof SelectContent>;
54
+ }
55
+
56
+ export function SelectFilterControl({
57
+ field,
58
+ label,
59
+ options,
60
+ value,
61
+ onChange,
62
+ triggerProps,
63
+ contentProps,
64
+ }: SelectFilterControlProps) {
65
+ return (
66
+ <Select
67
+ value={value?.value ?? ALL_VALUE}
68
+ onValueChange={(v) => {
69
+ if (v === ALL_VALUE) {
70
+ onChange(undefined);
71
+ } else {
72
+ onChange({ field, label, type: "picklist", value: v });
73
+ }
74
+ }}
75
+ >
76
+ <SelectTrigger
77
+ id={`filter-${field}`}
78
+ {...triggerProps}
79
+ className={cn("w-full", triggerProps?.className)}
80
+ >
81
+ <SelectValue placeholder={`Select ${label.toLowerCase()}`} />
82
+ </SelectTrigger>
83
+ <SelectContent {...contentProps}>
84
+ <SelectItem value={ALL_VALUE}>All</SelectItem>
85
+ {options.map((opt) => (
86
+ <SelectItem key={opt.value} value={opt.value}>
87
+ {opt.label}
88
+ </SelectItem>
89
+ ))}
90
+ </SelectContent>
91
+ </Select>
92
+ );
93
+ }
@@ -0,0 +1,74 @@
1
+ import { Input } from "../../../../components/ui/input";
2
+ import { Label } from "../../../../components/ui/label";
3
+ import { cn } from "../../../../lib/utils";
4
+ import { useFilterField } from "../FilterContext";
5
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
6
+
7
+ interface TextFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
8
+ field: string;
9
+ label: string;
10
+ placeholder?: string;
11
+ helpText?: string;
12
+ }
13
+
14
+ export function TextFilter({
15
+ field,
16
+ label,
17
+ placeholder,
18
+ helpText,
19
+ className,
20
+ ...props
21
+ }: TextFilterProps) {
22
+ const { value, onChange } = useFilterField(field);
23
+ return (
24
+ <div className={cn("space-y-1.5", className)} {...props}>
25
+ <Label htmlFor={`filter-${field}`}>{label}</Label>
26
+ <TextFilterInput
27
+ field={field}
28
+ label={label}
29
+ placeholder={placeholder}
30
+ value={value}
31
+ onChange={onChange}
32
+ />
33
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ interface TextFilterInputProps extends Omit<
39
+ React.ComponentProps<typeof Input>,
40
+ "onChange" | "value"
41
+ > {
42
+ field: string;
43
+ label: string;
44
+ value: ActiveFilterValue | undefined;
45
+ onChange: (value: ActiveFilterValue | undefined) => void;
46
+ }
47
+
48
+ export function TextFilterInput({
49
+ field,
50
+ label,
51
+ value,
52
+ onChange,
53
+ className,
54
+ ...props
55
+ }: TextFilterInputProps) {
56
+ return (
57
+ <Input
58
+ id={`filter-${field}`}
59
+ type="text"
60
+ placeholder={props.placeholder ?? `Filter by ${label.toLowerCase()}...`}
61
+ value={value?.value ?? ""}
62
+ onChange={(e) => {
63
+ const v = e.target.value;
64
+ if (v) {
65
+ onChange({ field, label, type: "text", value: v });
66
+ } else {
67
+ onChange(undefined);
68
+ }
69
+ }}
70
+ className={cn(className)}
71
+ {...props}
72
+ />
73
+ );
74
+ }
@@ -0,0 +1,54 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ interface UseAsyncDataResult<T> {
4
+ data: T | null;
5
+ loading: boolean;
6
+ error: string | null;
7
+ }
8
+
9
+ /**
10
+ * Runs an async fetcher on mount and whenever `deps` change.
11
+ * Returns the loading/error/data state. Does not cache — every call
12
+ * to the fetcher hits the source directly.
13
+ *
14
+ * A cleanup flag prevents state updates if the component unmounts
15
+ * or deps change before the fetch completes (avoids React warnings
16
+ * and stale updates from out-of-order responses).
17
+ */
18
+ export function useAsyncData<T>(
19
+ fetcher: () => Promise<T>,
20
+ deps: React.DependencyList,
21
+ ): UseAsyncDataResult<T> {
22
+ const [data, setData] = useState<T | null>(null);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ // Re-create the fetcher reference only when deps change.
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps --- deps are explicitly managed by the caller
28
+ const memoizedFetcher = useCallback(fetcher, deps);
29
+
30
+ useEffect(() => {
31
+ // Guard against setting state after unmount or dep change.
32
+ let cancelled = false;
33
+ setLoading(true);
34
+ setError(null);
35
+
36
+ memoizedFetcher()
37
+ .then((result) => {
38
+ if (!cancelled) setData(result);
39
+ })
40
+ .catch((err) => {
41
+ console.error(err);
42
+ if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
43
+ })
44
+ .finally(() => {
45
+ if (!cancelled) setLoading(false);
46
+ });
47
+
48
+ return () => {
49
+ cancelled = true;
50
+ };
51
+ }, [memoizedFetcher]);
52
+
53
+ return { data, loading, error };
54
+ }