@salesforce/webapp-template-app-react-template-b2e-experimental 1.109.5 → 1.109.7

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 (102) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +5 -6
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/api/objectSearchService.ts +84 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/ActiveFilters.tsx +89 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/FilterPanel.tsx +127 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/PaginationControls.tsx +151 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/SearchBar.tsx +41 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/SortControl.tsx +143 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useAsyncData.ts +53 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/debounce.ts +22 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/fieldUtils.ts +29 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/filterUtils.ts +372 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/sortUtils.ts +38 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/index.ts +3 -117
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Home.tsx +10 -11
  37. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/routes.tsx +8 -20
  38. package/dist/package-lock.json +2 -2
  39. package/dist/package.json +1 -1
  40. package/package.json +1 -1
  41. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectDetailService.ts +0 -102
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectInfoService.ts +0 -95
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/Section.tsx +0 -108
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterField.tsx +0 -54
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/forms/filters-form.tsx +0 -114
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/forms/submit-button.tsx +0 -47
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/constants.ts +0 -39
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/filters/FilterInput.tsx +0 -55
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/filters/FilterSelect.tsx +0 -72
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/form.tsx +0 -209
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/DetailPage.tsx +0 -109
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/filters.ts +0 -121
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/picklist.ts +0 -6
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/schema.d.ts +0 -200
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/search/searchResults.ts +0 -229
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/apiUtils.ts +0 -59
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/cacheUtils.ts +0 -76
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/debounce.ts +0 -90
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/fieldUtils.ts +0 -354
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/filterUtils.ts +0 -32
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/formUtils.ts +0 -142
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/linkUtils.ts +0 -14
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/paginationUtils.ts +0 -49
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/recordUtils.ts +0 -159
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/sanitizationUtils.ts +0 -50
@@ -0,0 +1,151 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ Pagination,
4
+ PaginationContent,
5
+ PaginationItem,
6
+ PaginationPrevious,
7
+ PaginationNext,
8
+ } from "../../../components/ui/pagination";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "../../../components/ui/select";
16
+ import { Label } from "../../../components/ui/label";
17
+ import { Button } from "../../../components/ui/button";
18
+
19
+ /** Shared props: pagination state is from useObjectSearchParams (synced to URL). */
20
+ interface PaginationControlsBase {
21
+ pageSize: number;
22
+ pageSizeOptions: readonly number[];
23
+ onPageSizeChange: (newPageSize: number) => void;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ /** Default mode: Previous, Page N, Next (cursor-based, state in URL). */
28
+ interface PaginationControlsDefaultProps extends PaginationControlsBase {
29
+ variant?: "default";
30
+ pageIndex: number;
31
+ hasNextPage: boolean;
32
+ hasPreviousPage: boolean;
33
+ onNextPage: () => void;
34
+ onPreviousPage: () => void;
35
+ }
36
+
37
+ /** Load More mode: optional page size + Load More button (or custom slot). */
38
+ interface PaginationControlsLoadMoreProps extends PaginationControlsBase {
39
+ variant: "loadMore";
40
+ hasNextPage: boolean;
41
+ onLoadMore: () => void;
42
+ loadMoreLoading?: boolean;
43
+ /** Custom content for load-more (e.g. custom button). If not set, renders default Load More button. */
44
+ loadMoreSlot?: ReactNode;
45
+ }
46
+
47
+ export type PaginationControlsProps =
48
+ | PaginationControlsDefaultProps
49
+ | PaginationControlsLoadMoreProps;
50
+
51
+ function isLoadMoreProps(props: PaginationControlsProps): props is PaginationControlsLoadMoreProps {
52
+ return props.variant === "loadMore";
53
+ }
54
+
55
+ export default function PaginationControls(props: PaginationControlsProps) {
56
+ const { pageSize, pageSizeOptions, onPageSizeChange, disabled = false } = props;
57
+
58
+ const handlePageSizeChange = (newValue: string) => {
59
+ const newSize = parseInt(newValue, 10);
60
+ if (!isNaN(newSize) && newSize !== pageSize) {
61
+ onPageSizeChange(newSize);
62
+ }
63
+ };
64
+
65
+ const pageSizeBlock = (
66
+ <div
67
+ className="flex justify-center sm:justify-start items-center gap-2 shrink-0"
68
+ role="group"
69
+ aria-label="Page size selector"
70
+ >
71
+ <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
72
+ Results per page:
73
+ </Label>
74
+ <Select value={pageSize.toString()} onValueChange={handlePageSizeChange} disabled={disabled}>
75
+ <SelectTrigger
76
+ id="page-size-select"
77
+ className="w-16"
78
+ aria-label="Select number of results per page"
79
+ >
80
+ <SelectValue />
81
+ </SelectTrigger>
82
+ <SelectContent>
83
+ {pageSizeOptions.map((size) => (
84
+ <SelectItem key={size} value={size.toString()}>
85
+ {size}
86
+ </SelectItem>
87
+ ))}
88
+ </SelectContent>
89
+ </Select>
90
+ </div>
91
+ );
92
+
93
+ if (isLoadMoreProps(props)) {
94
+ const { hasNextPage, onLoadMore, loadMoreLoading = false, loadMoreSlot } = props;
95
+ return (
96
+ <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
97
+ {pageSizeBlock}
98
+ <div className="flex justify-center sm:justify-end">
99
+ {loadMoreSlot !== undefined ? (
100
+ loadMoreSlot
101
+ ) : hasNextPage ? (
102
+ <Button
103
+ onClick={onLoadMore}
104
+ disabled={loadMoreLoading}
105
+ aria-label={loadMoreLoading ? "Loading..." : "Load More"}
106
+ >
107
+ {loadMoreLoading ? "Loading..." : "Load More"}
108
+ </Button>
109
+ ) : null}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ const { pageIndex, hasNextPage, hasPreviousPage, onNextPage, onPreviousPage } = props;
116
+ const currentPage = pageIndex + 1;
117
+ const prevDisabled = disabled || !hasPreviousPage;
118
+ const nextDisabled = disabled || !hasNextPage;
119
+
120
+ return (
121
+ <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
122
+ {pageSizeBlock}
123
+ <Pagination className="w-full mx-0 sm:justify-end">
124
+ <PaginationContent>
125
+ <PaginationItem>
126
+ <PaginationPrevious
127
+ onClick={prevDisabled ? undefined : onPreviousPage}
128
+ aria-disabled={prevDisabled}
129
+ className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
130
+ />
131
+ </PaginationItem>
132
+ <PaginationItem>
133
+ <span
134
+ className="min-w-16 text-center text-sm text-muted-foreground px-2"
135
+ aria-current="page"
136
+ >
137
+ Page {currentPage}
138
+ </span>
139
+ </PaginationItem>
140
+ <PaginationItem>
141
+ <PaginationNext
142
+ onClick={nextDisabled ? undefined : onNextPage}
143
+ aria-disabled={nextDisabled}
144
+ className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
145
+ />
146
+ </PaginationItem>
147
+ </PaginationContent>
148
+ </Pagination>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,41 @@
1
+ import { Search } from "lucide-react";
2
+ import { Input } from "../../../components/ui/input";
3
+ import { cn } from "../../../lib/utils";
4
+
5
+ interface SearchBarProps extends React.ComponentProps<"div"> {
6
+ value: string;
7
+ handleChange: (value: string) => void;
8
+ placeholder?: string;
9
+ iconProps?: React.ComponentProps<typeof Search>;
10
+ inputProps?: Omit<React.ComponentProps<typeof Input>, "value">;
11
+ }
12
+
13
+ export function SearchBar({
14
+ value,
15
+ handleChange,
16
+ placeholder,
17
+ className,
18
+ iconProps,
19
+ inputProps,
20
+ ...props
21
+ }: SearchBarProps) {
22
+ return (
23
+ <div className={cn("relative flex-1", className)} title={placeholder} {...props}>
24
+ <Search
25
+ {...iconProps}
26
+ className={cn(
27
+ "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
28
+ iconProps?.className,
29
+ )}
30
+ />
31
+ <Input
32
+ type="text"
33
+ value={value}
34
+ onChange={(e) => handleChange(e.target.value)}
35
+ placeholder={placeholder}
36
+ {...inputProps}
37
+ className={cn("pl-9", inputProps?.className)}
38
+ />
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,143 @@
1
+ import { ArrowUp, ArrowDown } from "lucide-react";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "../../../components/ui/select";
9
+ import { Button } from "../../../components/ui/button";
10
+ import { cn } from "../../../lib/utils";
11
+ import type { SortFieldConfig, SortState } from "../utils/sortUtils";
12
+
13
+ const NONE_VALUE = "__none__";
14
+
15
+ interface SortControlProps extends React.ComponentProps<"div"> {
16
+ configs: SortFieldConfig[];
17
+ sort: SortState | null;
18
+ onSortChange: (sort: SortState | null) => void;
19
+ labelProps?: React.ComponentProps<"span">;
20
+ selectProps?: Omit<
21
+ React.ComponentProps<typeof SortControlSelect>,
22
+ "configs" | "sort" | "onSortChange"
23
+ >;
24
+ directionButtonProps?: Omit<
25
+ React.ComponentProps<typeof SortDirectionButton>,
26
+ "sort" | "onSortChange"
27
+ >;
28
+ }
29
+
30
+ export function SortControl({
31
+ configs,
32
+ sort,
33
+ onSortChange,
34
+ className,
35
+ labelProps,
36
+ selectProps,
37
+ directionButtonProps,
38
+ ...props
39
+ }: SortControlProps) {
40
+ return (
41
+ <div className={cn("flex items-center gap-2", className)} {...props}>
42
+ <span
43
+ {...labelProps}
44
+ className={cn("text-sm text-muted-foreground whitespace-nowrap", labelProps?.className)}
45
+ >
46
+ {labelProps?.children ?? "Sort by"}
47
+ </span>
48
+ <SortControlSelect
49
+ configs={configs}
50
+ sort={sort}
51
+ onSortChange={onSortChange}
52
+ {...selectProps}
53
+ />
54
+ {sort && (
55
+ <SortDirectionButton sort={sort} onSortChange={onSortChange} {...directionButtonProps} />
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ interface SortControlSelectProps {
62
+ configs: SortFieldConfig[];
63
+ sort: SortState | null;
64
+ onSortChange: (sort: SortState | null) => void;
65
+ triggerProps?: React.ComponentProps<typeof SelectTrigger>;
66
+ contentProps?: React.ComponentProps<typeof SelectContent>;
67
+ selectValueProps?: React.ComponentProps<typeof SelectValue>;
68
+ selectItemProps?: Omit<React.ComponentProps<typeof SelectItem>, "value">;
69
+ }
70
+
71
+ export function SortControlSelect({
72
+ configs,
73
+ sort,
74
+ onSortChange,
75
+ triggerProps,
76
+ contentProps,
77
+ selectValueProps,
78
+ selectItemProps,
79
+ }: SortControlSelectProps) {
80
+ return (
81
+ <Select
82
+ value={sort?.field ?? NONE_VALUE}
83
+ onValueChange={(v) => {
84
+ if (v === NONE_VALUE) {
85
+ onSortChange(null);
86
+ } else {
87
+ onSortChange({
88
+ field: v,
89
+ direction: sort?.direction ?? "ASC",
90
+ });
91
+ }
92
+ }}
93
+ >
94
+ <SelectTrigger
95
+ size="sm"
96
+ {...triggerProps}
97
+ className={cn("w-[160px]", triggerProps?.className)}
98
+ >
99
+ <SelectValue placeholder="Default" {...selectValueProps} />
100
+ </SelectTrigger>
101
+ <SelectContent {...contentProps}>
102
+ <SelectItem value={NONE_VALUE} {...selectItemProps}>
103
+ Default
104
+ </SelectItem>
105
+ {configs.map((c) => (
106
+ <SelectItem key={c.field} value={c.field} {...selectItemProps}>
107
+ {c.label}
108
+ </SelectItem>
109
+ ))}
110
+ </SelectContent>
111
+ </Select>
112
+ );
113
+ }
114
+
115
+ interface SortDirectionButtonProps extends React.ComponentProps<typeof Button> {
116
+ sort: SortState;
117
+ onSortChange: (sort: SortState) => void;
118
+ }
119
+
120
+ export function SortDirectionButton({
121
+ sort,
122
+ onSortChange,
123
+ className,
124
+ ...props
125
+ }: SortDirectionButtonProps) {
126
+ return (
127
+ <Button
128
+ variant="ghost"
129
+ size="icon-sm"
130
+ className={cn(className)}
131
+ onClick={() =>
132
+ onSortChange({
133
+ ...sort,
134
+ direction: sort.direction === "ASC" ? "DESC" : "ASC",
135
+ })
136
+ }
137
+ aria-label={`Sort ${sort.direction === "ASC" ? "descending" : "ascending"}`}
138
+ {...props}
139
+ >
140
+ {sort.direction === "ASC" ? <ArrowUp /> : <ArrowDown />}
141
+ </Button>
142
+ );
143
+ }
@@ -0,0 +1,94 @@
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 type { FilterFieldConfig, ActiveFilterValue } from "../../utils/filterUtils";
11
+
12
+ const ALL_VALUE = "__all__";
13
+
14
+ interface BooleanFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
15
+ config: FilterFieldConfig;
16
+ value: ActiveFilterValue | undefined;
17
+ onChange: (value: ActiveFilterValue | undefined) => void;
18
+ labelProps?: React.ComponentProps<typeof Label>;
19
+ controlProps?: Omit<
20
+ React.ComponentProps<typeof BooleanFilterSelect>,
21
+ "config" | "value" | "onChange"
22
+ >;
23
+ helpTextProps?: React.ComponentProps<"p">;
24
+ }
25
+
26
+ export function BooleanFilter({
27
+ config,
28
+ value,
29
+ onChange,
30
+ className,
31
+ labelProps,
32
+ controlProps,
33
+ helpTextProps,
34
+ ...props
35
+ }: BooleanFilterProps) {
36
+ return (
37
+ <div className={cn("space-y-1.5", className)} {...props}>
38
+ <Label htmlFor={`filter-${config.field}`} {...labelProps}>
39
+ {labelProps?.children ?? config.label}
40
+ </Label>
41
+ <BooleanFilterSelect config={config} value={value} onChange={onChange} {...controlProps} />
42
+ {config.helpText && (
43
+ <p
44
+ {...helpTextProps}
45
+ className={cn("text-xs text-muted-foreground", helpTextProps?.className)}
46
+ >
47
+ {helpTextProps?.children ?? config.helpText}
48
+ </p>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ interface BooleanFilterSelectProps {
55
+ config: FilterFieldConfig;
56
+ value: ActiveFilterValue | undefined;
57
+ onChange: (value: ActiveFilterValue | undefined) => void;
58
+ triggerProps?: React.ComponentProps<typeof SelectTrigger>;
59
+ contentProps?: React.ComponentProps<typeof SelectContent>;
60
+ }
61
+
62
+ export function BooleanFilterSelect({
63
+ config,
64
+ value,
65
+ onChange,
66
+ triggerProps,
67
+ contentProps,
68
+ }: BooleanFilterSelectProps) {
69
+ return (
70
+ <Select
71
+ value={value?.value ?? ALL_VALUE}
72
+ onValueChange={(v) => {
73
+ if (v === ALL_VALUE) {
74
+ onChange(undefined);
75
+ } else {
76
+ onChange({ field: config.field, label: config.label, type: "boolean", value: v });
77
+ }
78
+ }}
79
+ >
80
+ <SelectTrigger
81
+ id={`filter-${config.field}`}
82
+ {...triggerProps}
83
+ className={cn("w-full", triggerProps?.className)}
84
+ >
85
+ <SelectValue />
86
+ </SelectTrigger>
87
+ <SelectContent {...contentProps}>
88
+ <SelectItem value={ALL_VALUE}>All</SelectItem>
89
+ <SelectItem value="true">Yes</SelectItem>
90
+ <SelectItem value="false">No</SelectItem>
91
+ </SelectContent>
92
+ </Select>
93
+ );
94
+ }
@@ -0,0 +1,138 @@
1
+ import { useState } from "react";
2
+ import { parseISO } from "date-fns";
3
+ import { Label } from "../../../../components/ui/label";
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "../../../../components/ui/select";
11
+ import {
12
+ DatePicker,
13
+ DatePickerTrigger,
14
+ DatePickerContent,
15
+ DatePickerCalendar,
16
+ } from "../../../../components/ui/datePicker";
17
+ import { cn } from "../../../../lib/utils";
18
+ import type { FilterFieldConfig, ActiveFilterValue } from "../../utils/filterUtils";
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
+ /** Maps operator to the ActiveFilterValue field used to carry the date. */
28
+ function operatorToField(op: DateOperator): "min" | "max" {
29
+ return op === "gt" ? "min" : "max";
30
+ }
31
+
32
+ interface DateFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
33
+ config: FilterFieldConfig;
34
+ value: ActiveFilterValue | undefined;
35
+ onChange: (value: ActiveFilterValue | undefined) => void;
36
+ labelProps?: React.ComponentProps<typeof Label>;
37
+ helpTextProps?: React.ComponentProps<"p">;
38
+ }
39
+
40
+ export function DateFilter({
41
+ config,
42
+ value,
43
+ onChange,
44
+ className,
45
+ labelProps,
46
+ helpTextProps,
47
+ ...props
48
+ }: DateFilterProps) {
49
+ // Derive initial operator from the existing value (min → gt, max → lt)
50
+ const initialOp: DateOperator = value?.min ? "gt" : "lt";
51
+ const [operator, setOperator] = useState<DateOperator>(initialOp);
52
+
53
+ const currentDate = toDate(value?.min ?? value?.max);
54
+
55
+ function handleOperatorChange(op: DateOperator) {
56
+ setOperator(op);
57
+ if (currentDate) {
58
+ emitChange(op, currentDate);
59
+ }
60
+ }
61
+
62
+ function handleDateChange(date: Date | undefined) {
63
+ if (!date) {
64
+ onChange(undefined);
65
+ } else {
66
+ emitChange(operator, date);
67
+ }
68
+ }
69
+
70
+ function emitChange(op: DateOperator, date: Date) {
71
+ const dateStr = toDateString(date);
72
+ const field = operatorToField(op);
73
+ onChange({
74
+ field: config.field,
75
+ label: config.label,
76
+ type: "date",
77
+ value: op,
78
+ min: field === "min" ? dateStr : undefined,
79
+ max: field === "max" ? dateStr : undefined,
80
+ });
81
+ }
82
+
83
+ return (
84
+ <div className={cn("space-y-1.5", className)} {...props}>
85
+ <Label {...labelProps}>{labelProps?.children ?? config.label}</Label>
86
+ <div className="flex gap-2">
87
+ <Select value={operator} onValueChange={(v) => handleOperatorChange(v as DateOperator)}>
88
+ <SelectTrigger className="w-full flex-1">
89
+ <SelectValue />
90
+ </SelectTrigger>
91
+ <SelectContent>
92
+ {OPERATOR_OPTIONS.map((opt) => (
93
+ <SelectItem key={opt.value} value={opt.value}>
94
+ {opt.label}
95
+ </SelectItem>
96
+ ))}
97
+ </SelectContent>
98
+ </Select>
99
+ <DatePicker>
100
+ <DatePickerTrigger
101
+ className="w-full flex-2"
102
+ date={currentDate}
103
+ dateFormat="MMM do, yyyy"
104
+ placeholder="Pick a date"
105
+ aria-label={config.label}
106
+ />
107
+ <DatePickerContent>
108
+ <DatePickerCalendar
109
+ mode="single"
110
+ captionLayout="dropdown"
111
+ selected={currentDate}
112
+ onSelect={handleDateChange}
113
+ />
114
+ </DatePickerContent>
115
+ </DatePicker>
116
+ </div>
117
+ {config.helpText && (
118
+ <p
119
+ {...helpTextProps}
120
+ className={cn("text-xs text-muted-foreground", helpTextProps?.className)}
121
+ >
122
+ {helpTextProps?.children ?? config.helpText}
123
+ </p>
124
+ )}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ export function toDate(value: string | undefined): Date | undefined {
130
+ if (!value) return undefined;
131
+ const parsed = parseISO(value);
132
+ return isNaN(parsed.getTime()) ? undefined : parsed;
133
+ }
134
+
135
+ export function toDateString(date: Date | undefined): string {
136
+ if (!date) return "";
137
+ return date.toISOString().split("T")[0];
138
+ }
@@ -0,0 +1,78 @@
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 type { FilterFieldConfig, ActiveFilterValue } from "../../utils/filterUtils";
11
+ import { toDate, toDateString } from "./DateFilter";
12
+
13
+ interface DateRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
14
+ config: FilterFieldConfig;
15
+ value: ActiveFilterValue | undefined;
16
+ onChange: (value: ActiveFilterValue | undefined) => void;
17
+ labelProps?: React.ComponentProps<typeof Label>;
18
+ helpTextProps?: React.ComponentProps<"p">;
19
+ }
20
+
21
+ export function DateRangeFilter({
22
+ config,
23
+ value,
24
+ onChange,
25
+ className,
26
+ labelProps,
27
+ helpTextProps,
28
+ ...props
29
+ }: DateRangeFilterProps) {
30
+ const dateRange: DateRange | undefined =
31
+ value?.min || value?.max ? { from: toDate(value?.min), to: toDate(value?.max) } : undefined;
32
+
33
+ function handleRangeSelect(range: DateRange | undefined) {
34
+ if (!range?.from && !range?.to) {
35
+ onChange(undefined);
36
+ } else {
37
+ onChange({
38
+ field: config.field,
39
+ label: config.label,
40
+ type: "daterange",
41
+ min: toDateString(range?.from),
42
+ max: toDateString(range?.to),
43
+ });
44
+ }
45
+ }
46
+
47
+ return (
48
+ <div className={cn("space-y-1.5", className)} {...props}>
49
+ <Label {...labelProps}>{labelProps?.children ?? config.label}</Label>
50
+ <DatePicker>
51
+ <DatePickerRangeTrigger
52
+ className="w-full"
53
+ dateRange={dateRange}
54
+ placeholder="Pick a date range"
55
+ aria-label={config.label}
56
+ />
57
+ <DatePickerContent align="start">
58
+ <DatePickerCalendar
59
+ mode="range"
60
+ captionLayout="dropdown"
61
+ defaultMonth={dateRange?.from}
62
+ selected={dateRange}
63
+ onSelect={handleRangeSelect}
64
+ numberOfMonths={2}
65
+ />
66
+ </DatePickerContent>
67
+ </DatePicker>
68
+ {config.helpText && (
69
+ <p
70
+ {...helpTextProps}
71
+ className={cn("text-xs text-muted-foreground", helpTextProps?.className)}
72
+ >
73
+ {helpTextProps?.children ?? config.helpText}
74
+ </p>
75
+ )}
76
+ </div>
77
+ );
78
+ }