@salesforce/webapp-template-app-react-template-b2x-experimental 1.109.5 → 1.109.6

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 +8 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package.json +4 -3
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/FilterPanel.tsx +127 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/PaginationControls.tsx +151 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SortControl.tsx +143 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useAsyncData.ts +53 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/debounce.ts +22 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/Home.tsx +10 -11
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/routes.tsx +8 -20
  37. package/dist/package-lock.json +2 -2
  38. package/dist/package.json +1 -1
  39. package/package.json +1 -1
  40. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  41. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/constants.ts +0 -39
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/form.tsx +0 -209
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/filters.ts +0 -121
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/schema.d.ts +0 -200
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/search/searchResults.ts +0 -229
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/debounce.ts +0 -90
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formUtils.ts +0 -142
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/index.ts +0 -120
@@ -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
+ }