@salesforce/ui-bundle-template-app-react-template-b2x 11.17.1 → 11.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/uiBundles/reactexternalapp/package.json +4 -4
  3. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/components → components}/ObjectBreadcrumb.tsx +7 -2
  4. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/distinctValuesService.ts +84 -0
  5. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/searchService.ts +71 -0
  6. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/MergedSearchResults.tsx +263 -0
  7. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/Search.tsx +185 -0
  8. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SearchResults.tsx +77 -0
  9. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SourceSection.tsx +166 -0
  10. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
  11. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  12. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SearchBar.tsx +55 -0
  13. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SortControl.tsx +62 -0
  14. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  15. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
  16. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
  17. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  18. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
  19. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  20. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
  21. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  22. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  23. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  24. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  25. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/config.json +83 -0
  26. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
  27. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useDistinctValues.ts +59 -0
  28. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useSearch.ts +703 -0
  29. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/index.ts +90 -0
  30. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/loadConfig.ts +14 -0
  31. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts +156 -0
  32. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/types.ts +307 -0
  33. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/debounce.ts +17 -0
  34. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/fieldUtils.ts +14 -0
  35. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/filterUtils.ts +285 -0
  36. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/sortUtils.ts +34 -0
  37. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +49 -16
  38. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +21 -293
  39. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/Home.tsx +22 -7
  40. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +1 -1
  41. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/utils/fieldUtils.ts → utils/accountFields.ts} +9 -0
  42. package/dist/force-app/main/default/uiBundles/reactexternalapp/tsconfig.tsbuildinfo +1 -1
  43. package/dist/package-lock.json +2 -2
  44. package/dist/package.json +1 -1
  45. package/package.json +1 -1
  46. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/accountSearchService.ts +0 -46
  47. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountIndustries.graphql +0 -19
  48. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountTypes.graphql +0 -19
  49. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/searchAccounts.graphql +0 -51
  50. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/graphql-operations-types.ts +0 -11260
  51. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  52. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  53. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  54. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  55. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  56. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
  57. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
  58. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
  59. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/api/objectSearchService.ts +0 -82
  60. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
  61. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/PaginationControls.tsx +0 -109
  62. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SearchBar.tsx +0 -41
  63. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SortControl.tsx +0 -143
  64. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  65. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
  66. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  67. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
  68. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
  69. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
  70. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
  71. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
  72. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
  73. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  74. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
  75. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/debounce.ts +0 -25
  76. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/filterUtils.ts +0 -404
  77. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/sortUtils.ts +0 -38
@@ -1,143 +0,0 @@
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
- }
@@ -1,78 +0,0 @@
1
- import {
2
- Select,
3
- SelectContent,
4
- SelectItem,
5
- SelectTrigger,
6
- SelectValue,
7
- } from "../../../../components/ui/select";
8
- import { cn } from "../../../../lib/utils";
9
- import { useFilterField } from "../FilterContext";
10
- import { FilterFieldWrapper } from "./FilterFieldWrapper";
11
- import type { ActiveFilterValue } from "../../utils/filterUtils";
12
-
13
- const ALL_VALUE = "__all__";
14
-
15
- interface BooleanFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
16
- field: string;
17
- label: string;
18
- helpText?: string;
19
- }
20
-
21
- export function BooleanFilter({ field, label, helpText, className, ...props }: BooleanFilterProps) {
22
- const { value, onChange } = useFilterField(field);
23
- return (
24
- <FilterFieldWrapper
25
- label={label}
26
- htmlFor={`filter-${field}`}
27
- helpText={helpText}
28
- className={className}
29
- {...props}
30
- >
31
- <BooleanFilterSelect field={field} label={label} value={value} onChange={onChange} />
32
- </FilterFieldWrapper>
33
- );
34
- }
35
-
36
- interface BooleanFilterSelectProps {
37
- field: string;
38
- label: string;
39
- value: ActiveFilterValue | undefined;
40
- onChange: (value: ActiveFilterValue | undefined) => void;
41
- triggerProps?: React.ComponentProps<typeof SelectTrigger>;
42
- contentProps?: React.ComponentProps<typeof SelectContent>;
43
- }
44
-
45
- export function BooleanFilterSelect({
46
- field,
47
- label,
48
- value,
49
- onChange,
50
- triggerProps,
51
- contentProps,
52
- }: BooleanFilterSelectProps) {
53
- return (
54
- <Select
55
- value={value?.value ?? ALL_VALUE}
56
- onValueChange={(v) => {
57
- if (v === ALL_VALUE) {
58
- onChange(undefined);
59
- } else {
60
- onChange({ field, label, type: "boolean", value: v });
61
- }
62
- }}
63
- >
64
- <SelectTrigger
65
- id={`filter-${field}`}
66
- {...triggerProps}
67
- className={cn("w-full", triggerProps?.className)}
68
- >
69
- <SelectValue />
70
- </SelectTrigger>
71
- <SelectContent {...contentProps}>
72
- <SelectItem value={ALL_VALUE}>All</SelectItem>
73
- <SelectItem value="true">Yes</SelectItem>
74
- <SelectItem value="false">No</SelectItem>
75
- </SelectContent>
76
- </Select>
77
- );
78
- }
@@ -1,128 +0,0 @@
1
- import { useState } from "react";
2
- import { parseISO } from "date-fns";
3
- import {
4
- DatePicker,
5
- DatePickerTrigger,
6
- DatePickerContent,
7
- DatePickerCalendar,
8
- } from "../../../../components/ui/datePicker";
9
-
10
- import { useFilterField } from "../FilterContext";
11
- import { FilterFieldWrapper } from "./FilterFieldWrapper";
12
- import type { FilterFieldType } from "../../utils/filterUtils";
13
- import {
14
- Select,
15
- SelectContent,
16
- SelectItem,
17
- SelectTrigger,
18
- SelectValue,
19
- } from "../../../../components/ui/select";
20
-
21
- type DateOperator = "gt" | "lt";
22
-
23
- const OPERATOR_OPTIONS: { value: DateOperator; label: string }[] = [
24
- { value: "gt", label: "After" },
25
- { value: "lt", label: "Before" },
26
- ];
27
-
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
- field: string;
34
- label: string;
35
- helpText?: string;
36
- filterType?: FilterFieldType;
37
- }
38
-
39
- export function DateFilter({
40
- field,
41
- label,
42
- helpText,
43
- filterType = "date",
44
- className,
45
- ...props
46
- }: DateFilterProps) {
47
- const { value, onChange } = useFilterField(field);
48
-
49
- const initialOp: DateOperator = value?.min ? "gt" : "lt";
50
- const [operator, setOperator] = useState<DateOperator>(initialOp);
51
-
52
- const currentDate = toDate(value?.min ?? value?.max);
53
-
54
- function handleOperatorChange(op: DateOperator) {
55
- setOperator(op);
56
- if (currentDate) {
57
- emitChange(op, currentDate);
58
- }
59
- }
60
-
61
- function handleDateChange(date: Date | undefined) {
62
- if (!date) {
63
- onChange(undefined);
64
- } else {
65
- emitChange(operator, date);
66
- }
67
- }
68
-
69
- function emitChange(op: DateOperator, date: Date) {
70
- const dateStr = toDateString(date);
71
- const f = operatorToField(op);
72
- onChange({
73
- field,
74
- label,
75
- type: filterType,
76
- value: op,
77
- min: f === "min" ? dateStr : undefined,
78
- max: f === "max" ? dateStr : undefined,
79
- });
80
- }
81
-
82
- return (
83
- <FilterFieldWrapper label={label} helpText={helpText} className={className} {...props}>
84
- <div className="flex gap-2">
85
- <Select value={operator} onValueChange={(v) => handleOperatorChange(v as DateOperator)}>
86
- <SelectTrigger className="w-full flex-1" aria-label={label}>
87
- <SelectValue />
88
- </SelectTrigger>
89
- <SelectContent>
90
- {OPERATOR_OPTIONS.map((opt) => (
91
- <SelectItem key={opt.value} value={opt.value}>
92
- {opt.label}
93
- </SelectItem>
94
- ))}
95
- </SelectContent>
96
- </Select>
97
- <DatePicker>
98
- <DatePickerTrigger
99
- className="w-full flex-2"
100
- date={currentDate}
101
- dateFormat="MMM do, yyyy"
102
- placeholder="Pick a date"
103
- aria-label={label}
104
- />
105
- <DatePickerContent>
106
- <DatePickerCalendar
107
- mode="single"
108
- captionLayout="dropdown"
109
- selected={currentDate}
110
- onSelect={handleDateChange}
111
- />
112
- </DatePickerContent>
113
- </DatePicker>
114
- </div>
115
- </FilterFieldWrapper>
116
- );
117
- }
118
-
119
- export function toDate(value: string | undefined): Date | undefined {
120
- if (!value) return undefined;
121
- const parsed = parseISO(value);
122
- return isNaN(parsed.getTime()) ? undefined : parsed;
123
- }
124
-
125
- export function toDateString(date: Date | undefined): string {
126
- if (!date) return "";
127
- return date.toISOString().split("T")[0];
128
- }
@@ -1,70 +0,0 @@
1
- import type { DateRange } from "react-day-picker";
2
- import {
3
- DatePicker,
4
- DatePickerRangeTrigger,
5
- DatePickerContent,
6
- DatePickerCalendar,
7
- } from "../../../../components/ui/datePicker";
8
-
9
- import { useFilterField } from "../FilterContext";
10
- import { FilterFieldWrapper } from "./FilterFieldWrapper";
11
- import type { FilterFieldType } from "../../utils/filterUtils";
12
- import { toDate, toDateString } from "./DateFilter";
13
-
14
- interface DateRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
15
- field: string;
16
- label: string;
17
- helpText?: string;
18
- filterType?: FilterFieldType;
19
- }
20
-
21
- export function DateRangeFilter({
22
- field,
23
- label,
24
- helpText,
25
- filterType = "daterange",
26
- className,
27
- ...props
28
- }: DateRangeFilterProps) {
29
- const { value, onChange } = useFilterField(field);
30
-
31
- const dateRange: DateRange | undefined =
32
- value?.min || value?.max ? { from: toDate(value?.min), to: toDate(value?.max) } : undefined;
33
-
34
- function handleRangeSelect(range: DateRange | undefined) {
35
- if (!range?.from && !range?.to) {
36
- onChange(undefined);
37
- } else {
38
- onChange({
39
- field,
40
- label,
41
- type: filterType,
42
- min: toDateString(range?.from),
43
- max: toDateString(range?.to),
44
- });
45
- }
46
- }
47
-
48
- return (
49
- <FilterFieldWrapper label={label} helpText={helpText} className={className} {...props}>
50
- <DatePicker>
51
- <DatePickerRangeTrigger
52
- className="w-full"
53
- dateRange={dateRange}
54
- placeholder="Pick a date range"
55
- aria-label={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
- </FilterFieldWrapper>
69
- );
70
- }
@@ -1,43 +0,0 @@
1
- import { Label } from "../../../../components/ui/label";
2
- import { cn } from "../../../../lib/utils";
3
-
4
- interface FilterFieldWrapperProps extends React.ComponentProps<"div"> {
5
- label: string;
6
- htmlFor?: string;
7
- /** id applied to the visible label so a non-native control can reference it via aria-labelledby */
8
- labelId?: string;
9
- /** id applied to the error message so a control can reference it via aria-describedby */
10
- errorId?: string;
11
- helpText?: string;
12
- error?: string;
13
- }
14
-
15
- export function FilterFieldWrapper({
16
- label,
17
- htmlFor,
18
- labelId,
19
- errorId,
20
- helpText,
21
- error,
22
- className,
23
- children,
24
- ...props
25
- }: FilterFieldWrapperProps) {
26
- return (
27
- <div className={cn("space-y-1", className)} {...props}>
28
- <Label id={labelId} htmlFor={htmlFor}>
29
- {label}
30
- </Label>
31
- {children}
32
- <div className="min-h-4">
33
- {error ? (
34
- <p id={errorId} role="alert" className="text-xs text-destructive">
35
- {error}
36
- </p>
37
- ) : (
38
- helpText && <p className="text-xs text-muted-foreground">{helpText}</p>
39
- )}
40
- </div>
41
- </div>
42
- );
43
- }
@@ -1,105 +0,0 @@
1
- import {
2
- Popover,
3
- PopoverContent,
4
- PopoverTrigger,
5
- } from "../../../../components/ui/popover";
6
- import { Checkbox } from "../../../../components/ui/checkbox";
7
- import { Button } from "../../../../components/ui/button";
8
- import { cn } from "../../../../lib/utils";
9
- import { Label } from "../../../../components/ui/label";
10
- import { ChevronDown } from "lucide-react";
11
- import { useFilterField } from "../FilterContext";
12
- import { FilterFieldWrapper } from "./FilterFieldWrapper";
13
-
14
- interface MultiSelectFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
15
- field: string;
16
- label: string;
17
- options: Array<{ value: string; label: string }>;
18
- helpText?: string;
19
- }
20
-
21
- export function MultiSelectFilter({
22
- field,
23
- label,
24
- options,
25
- helpText,
26
- className,
27
- ...props
28
- }: MultiSelectFilterProps) {
29
- const { value, onChange } = useFilterField(field);
30
- const selected = value?.value ? value.value.split(",") : [];
31
- const labelId = `filter-${field}-label`;
32
-
33
- const triggerLabel =
34
- selected.length === 0
35
- ? `Select ${label.toLowerCase()}`
36
- : selected.length === 1
37
- ? (options.find((o) => o.value === selected[0])?.label ?? selected[0])
38
- : `${selected.length} selected`;
39
-
40
- function handleToggle(optionValue: string) {
41
- const next = selected.includes(optionValue)
42
- ? selected.filter((v) => v !== optionValue)
43
- : [...selected, optionValue];
44
-
45
- if (next.length === 0) {
46
- onChange(undefined);
47
- } else {
48
- onChange({
49
- field,
50
- label,
51
- type: "multipicklist",
52
- value: next.join(","),
53
- });
54
- }
55
- }
56
-
57
- return (
58
- <FilterFieldWrapper
59
- label={label}
60
- labelId={labelId}
61
- helpText={helpText}
62
- className={className}
63
- {...props}
64
- >
65
- <Popover>
66
- <PopoverTrigger asChild>
67
- <Button
68
- variant="outline"
69
- role="combobox"
70
- aria-labelledby={labelId}
71
- className={cn(
72
- "w-full justify-between font-normal",
73
- selected.length === 0 && "text-muted-foreground",
74
- )}
75
- >
76
- <span className="truncate">{triggerLabel}</span>
77
- <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
78
- </Button>
79
- </PopoverTrigger>
80
- <PopoverContent className="p-2" align="start">
81
- <div className="max-h-48 overflow-y-auto space-y-1">
82
- {options.map((opt) => {
83
- const id = `filter-${field}-${opt.value}`;
84
- return (
85
- <div
86
- key={opt.value}
87
- className="flex items-center gap-2 rounded px-1 py-0.5 hover:bg-accent"
88
- >
89
- <Checkbox
90
- id={id}
91
- checked={selected.includes(opt.value)}
92
- onCheckedChange={() => handleToggle(opt.value)}
93
- />
94
- <Label htmlFor={id} className="text-sm font-normal cursor-pointer w-full">
95
- {opt.label}
96
- </Label>
97
- </div>
98
- );
99
- })}
100
- </div>
101
- </PopoverContent>
102
- </Popover>
103
- </FilterFieldWrapper>
104
- );
105
- }