@salesforce/ui-bundle-template-app-react-sample-b2x 11.68.0 → 11.68.2
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.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/api/objectSearchService.ts +2 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useObjectSearchParams.ts +1 -19
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/utils/debounce.ts +1 -12
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/utils/filterUtils.ts +8 -123
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/utils/sortUtils.ts +1 -17
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +3 -6
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/routes.tsx +0 -10
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +4 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/FilterContext.tsx +0 -83
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/SearchBar.tsx +0 -41
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/SortControl.tsx +0 -143
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useAsyncData.ts +0 -63
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/utils/fieldUtils.ts +0 -29
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { useId, useState } from "react";
|
|
2
|
-
import { Input } from "../../../../components/ui/input";
|
|
3
|
-
|
|
4
|
-
import { useFilterField } from "../FilterContext";
|
|
5
|
-
import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
|
|
6
|
-
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
7
|
-
import type { ActiveFilterValue } from "../../utils/filterUtils";
|
|
8
|
-
|
|
9
|
-
interface NumericRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
10
|
-
field: string;
|
|
11
|
-
label: string;
|
|
12
|
-
helpText?: string;
|
|
13
|
-
min?: number;
|
|
14
|
-
max?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function NumericRangeFilter({
|
|
18
|
-
field,
|
|
19
|
-
label,
|
|
20
|
-
helpText,
|
|
21
|
-
min,
|
|
22
|
-
max,
|
|
23
|
-
className,
|
|
24
|
-
...props
|
|
25
|
-
}: NumericRangeFilterProps) {
|
|
26
|
-
const { value, onChange } = useFilterField(field);
|
|
27
|
-
return (
|
|
28
|
-
<NumericRangeFilterInputs
|
|
29
|
-
field={field}
|
|
30
|
-
label={label}
|
|
31
|
-
helpText={helpText}
|
|
32
|
-
value={value}
|
|
33
|
-
onChange={onChange}
|
|
34
|
-
min={min}
|
|
35
|
-
max={max}
|
|
36
|
-
className={className}
|
|
37
|
-
{...props}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface NumericRangeFilterInputsProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
43
|
-
field: string;
|
|
44
|
-
label: string;
|
|
45
|
-
helpText?: string;
|
|
46
|
-
value: ActiveFilterValue | undefined;
|
|
47
|
-
onChange: (value: ActiveFilterValue | undefined) => void;
|
|
48
|
-
min?: number;
|
|
49
|
-
max?: number;
|
|
50
|
-
minInputProps?: React.ComponentProps<typeof Input>;
|
|
51
|
-
maxInputProps?: React.ComponentProps<typeof Input>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function NumericRangeFilterInputs({
|
|
55
|
-
field,
|
|
56
|
-
label,
|
|
57
|
-
helpText,
|
|
58
|
-
value,
|
|
59
|
-
onChange,
|
|
60
|
-
min: boundMin,
|
|
61
|
-
max: boundMax,
|
|
62
|
-
className,
|
|
63
|
-
...props
|
|
64
|
-
}: NumericRangeFilterInputsProps) {
|
|
65
|
-
const errorId = useId();
|
|
66
|
-
const [localMin, setLocalMin] = useState(value?.min ?? "");
|
|
67
|
-
const [localMax, setLocalMax] = useState(value?.max ?? "");
|
|
68
|
-
|
|
69
|
-
const externalMin = value?.min ?? "";
|
|
70
|
-
const externalMax = value?.max ?? "";
|
|
71
|
-
const [prevExternalMin, setPrevExternalMin] = useState(externalMin);
|
|
72
|
-
const [prevExternalMax, setPrevExternalMax] = useState(externalMax);
|
|
73
|
-
if (prevExternalMin !== externalMin) {
|
|
74
|
-
setPrevExternalMin(externalMin);
|
|
75
|
-
setLocalMin(externalMin);
|
|
76
|
-
}
|
|
77
|
-
if (prevExternalMax !== externalMax) {
|
|
78
|
-
setPrevExternalMax(externalMax);
|
|
79
|
-
setLocalMax(externalMax);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const isOutOfBounds = (v: string) => {
|
|
83
|
-
if (v === "") return false;
|
|
84
|
-
const n = Number(v);
|
|
85
|
-
return (boundMin != null && n < boundMin) || (boundMax != null && n > boundMax);
|
|
86
|
-
};
|
|
87
|
-
const minOutOfBounds = isOutOfBounds(localMin);
|
|
88
|
-
const maxOutOfBounds = isOutOfBounds(localMax);
|
|
89
|
-
const isRangeInverted = localMin !== "" && localMax !== "" && Number(localMin) > Number(localMax);
|
|
90
|
-
const hasError = minOutOfBounds || maxOutOfBounds || isRangeInverted;
|
|
91
|
-
|
|
92
|
-
const debouncedOnChange = useDebouncedCallback((min: string, max: string) => {
|
|
93
|
-
if (!min && !max) {
|
|
94
|
-
onChange(undefined);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const minNum = min !== "" ? Number(min) : null;
|
|
98
|
-
const maxNum = max !== "" ? Number(max) : null;
|
|
99
|
-
if (minNum != null && maxNum != null && minNum > maxNum) return;
|
|
100
|
-
if (
|
|
101
|
-
minNum != null &&
|
|
102
|
-
((boundMin != null && minNum < boundMin) || (boundMax != null && minNum > boundMax))
|
|
103
|
-
)
|
|
104
|
-
return;
|
|
105
|
-
if (
|
|
106
|
-
maxNum != null &&
|
|
107
|
-
((boundMin != null && maxNum < boundMin) || (boundMax != null && maxNum > boundMax))
|
|
108
|
-
)
|
|
109
|
-
return;
|
|
110
|
-
onChange({ field, label, type: "numeric" as const, min, max });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const boundsLabel =
|
|
114
|
-
boundMin != null && boundMax != null
|
|
115
|
-
? `${boundMin}–${boundMax}`
|
|
116
|
-
: boundMin != null
|
|
117
|
-
? `${boundMin} or more`
|
|
118
|
-
: boundMax != null
|
|
119
|
-
? `${boundMax} or less`
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
const errorMessage = isRangeInverted
|
|
123
|
-
? "Min must not exceed max"
|
|
124
|
-
: (minOutOfBounds || maxOutOfBounds) && boundsLabel
|
|
125
|
-
? `Value must be between ${boundsLabel}`
|
|
126
|
-
: undefined;
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<FilterFieldWrapper
|
|
130
|
-
label={label}
|
|
131
|
-
helpText={helpText}
|
|
132
|
-
error={errorMessage}
|
|
133
|
-
errorId={errorId}
|
|
134
|
-
className={className}
|
|
135
|
-
{...props}
|
|
136
|
-
>
|
|
137
|
-
<div className="flex gap-2">
|
|
138
|
-
<Input
|
|
139
|
-
type="number"
|
|
140
|
-
placeholder="Min"
|
|
141
|
-
value={localMin}
|
|
142
|
-
min={boundMin}
|
|
143
|
-
max={boundMax}
|
|
144
|
-
onChange={(e) => {
|
|
145
|
-
const v = e.target.value;
|
|
146
|
-
setLocalMin(v);
|
|
147
|
-
debouncedOnChange(v, localMax);
|
|
148
|
-
}}
|
|
149
|
-
aria-label={`${label} minimum`}
|
|
150
|
-
aria-invalid={hasError || undefined}
|
|
151
|
-
aria-describedby={hasError ? errorId : undefined}
|
|
152
|
-
/>
|
|
153
|
-
<Input
|
|
154
|
-
type="number"
|
|
155
|
-
placeholder="Max"
|
|
156
|
-
value={localMax}
|
|
157
|
-
min={boundMin}
|
|
158
|
-
max={boundMax}
|
|
159
|
-
onChange={(e) => {
|
|
160
|
-
const v = e.target.value;
|
|
161
|
-
setLocalMax(v);
|
|
162
|
-
debouncedOnChange(localMin, v);
|
|
163
|
-
}}
|
|
164
|
-
aria-label={`${label} maximum`}
|
|
165
|
-
aria-invalid={hasError || undefined}
|
|
166
|
-
aria-describedby={hasError ? errorId : undefined}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
</FilterFieldWrapper>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
|
-
import { SearchBar } from "../SearchBar";
|
|
4
|
-
import { useFilterField } from "../FilterContext";
|
|
5
|
-
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
6
|
-
import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
|
|
7
|
-
|
|
8
|
-
interface SearchFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
9
|
-
field: string;
|
|
10
|
-
label: string;
|
|
11
|
-
placeholder?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function SearchFilter({
|
|
15
|
-
field,
|
|
16
|
-
label,
|
|
17
|
-
placeholder,
|
|
18
|
-
className,
|
|
19
|
-
...props
|
|
20
|
-
}: SearchFilterProps) {
|
|
21
|
-
const { value, onChange } = useFilterField(field);
|
|
22
|
-
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
23
|
-
|
|
24
|
-
const externalValue = value?.value ?? "";
|
|
25
|
-
const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
|
|
26
|
-
if (prevExternalValue !== externalValue) {
|
|
27
|
-
setPrevExternalValue(externalValue);
|
|
28
|
-
setLocalValue(externalValue);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
32
|
-
if (v) {
|
|
33
|
-
onChange({ field, label, type: "search", value: v });
|
|
34
|
-
} else {
|
|
35
|
-
onChange(undefined);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<FilterFieldWrapper label={label} htmlFor={`filter-${field}`} className={className} {...props}>
|
|
41
|
-
<SearchBar
|
|
42
|
-
value={localValue}
|
|
43
|
-
handleChange={(v) => {
|
|
44
|
-
setLocalValue(v);
|
|
45
|
-
debouncedOnChange(v);
|
|
46
|
-
}}
|
|
47
|
-
placeholder={placeholder}
|
|
48
|
-
inputProps={{ id: `filter-${field}` }}
|
|
49
|
-
/>
|
|
50
|
-
</FilterFieldWrapper>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
@@ -1,97 +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 SelectFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
16
|
-
field: string;
|
|
17
|
-
label: string;
|
|
18
|
-
options: Array<{ value: string; label: string }>;
|
|
19
|
-
helpText?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function SelectFilter({
|
|
23
|
-
field,
|
|
24
|
-
label,
|
|
25
|
-
options,
|
|
26
|
-
helpText,
|
|
27
|
-
className,
|
|
28
|
-
...props
|
|
29
|
-
}: SelectFilterProps) {
|
|
30
|
-
const { value, onChange } = useFilterField(field);
|
|
31
|
-
return (
|
|
32
|
-
<FilterFieldWrapper
|
|
33
|
-
label={label}
|
|
34
|
-
htmlFor={`filter-${field}`}
|
|
35
|
-
helpText={helpText}
|
|
36
|
-
className={className}
|
|
37
|
-
{...props}
|
|
38
|
-
>
|
|
39
|
-
<SelectFilterControl
|
|
40
|
-
field={field}
|
|
41
|
-
label={label}
|
|
42
|
-
options={options}
|
|
43
|
-
value={value}
|
|
44
|
-
onChange={onChange}
|
|
45
|
-
/>
|
|
46
|
-
</FilterFieldWrapper>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface SelectFilterControlProps {
|
|
51
|
-
field: string;
|
|
52
|
-
label: string;
|
|
53
|
-
options: Array<{ value: string; label: string }>;
|
|
54
|
-
value: ActiveFilterValue | undefined;
|
|
55
|
-
onChange: (value: ActiveFilterValue | undefined) => void;
|
|
56
|
-
triggerProps?: React.ComponentProps<typeof SelectTrigger>;
|
|
57
|
-
contentProps?: React.ComponentProps<typeof SelectContent>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function SelectFilterControl({
|
|
61
|
-
field,
|
|
62
|
-
label,
|
|
63
|
-
options,
|
|
64
|
-
value,
|
|
65
|
-
onChange,
|
|
66
|
-
triggerProps,
|
|
67
|
-
contentProps,
|
|
68
|
-
}: SelectFilterControlProps) {
|
|
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, label, type: "picklist", value: v });
|
|
77
|
-
}
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
<SelectTrigger
|
|
81
|
-
id={`filter-${field}`}
|
|
82
|
-
{...triggerProps}
|
|
83
|
-
className={cn("w-full", triggerProps?.className)}
|
|
84
|
-
>
|
|
85
|
-
<SelectValue placeholder={`Select ${label.toLowerCase()}`} />
|
|
86
|
-
</SelectTrigger>
|
|
87
|
-
<SelectContent {...contentProps}>
|
|
88
|
-
<SelectItem value={ALL_VALUE}>All</SelectItem>
|
|
89
|
-
{options.map((opt) => (
|
|
90
|
-
<SelectItem key={opt.value} value={opt.value}>
|
|
91
|
-
{opt.label}
|
|
92
|
-
</SelectItem>
|
|
93
|
-
))}
|
|
94
|
-
</SelectContent>
|
|
95
|
-
</Select>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Input } from "../../../../components/ui/input";
|
|
3
|
-
import { cn } from "../../../../lib/utils";
|
|
4
|
-
import { useFilterField } from "../FilterContext";
|
|
5
|
-
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
6
|
-
import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
|
|
7
|
-
import type { ActiveFilterValue } from "../../utils/filterUtils";
|
|
8
|
-
|
|
9
|
-
interface TextFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
10
|
-
field: string;
|
|
11
|
-
label: string;
|
|
12
|
-
placeholder?: string;
|
|
13
|
-
helpText?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function TextFilter({
|
|
17
|
-
field,
|
|
18
|
-
label,
|
|
19
|
-
placeholder,
|
|
20
|
-
helpText,
|
|
21
|
-
className,
|
|
22
|
-
...props
|
|
23
|
-
}: TextFilterProps) {
|
|
24
|
-
const { value, onChange } = useFilterField(field);
|
|
25
|
-
return (
|
|
26
|
-
<FilterFieldWrapper
|
|
27
|
-
label={label}
|
|
28
|
-
htmlFor={`filter-${field}`}
|
|
29
|
-
helpText={helpText}
|
|
30
|
-
className={className}
|
|
31
|
-
{...props}
|
|
32
|
-
>
|
|
33
|
-
<TextFilterInput
|
|
34
|
-
field={field}
|
|
35
|
-
label={label}
|
|
36
|
-
placeholder={placeholder}
|
|
37
|
-
value={value}
|
|
38
|
-
onChange={onChange}
|
|
39
|
-
/>
|
|
40
|
-
</FilterFieldWrapper>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface TextFilterInputProps extends Omit<
|
|
45
|
-
React.ComponentProps<typeof Input>,
|
|
46
|
-
"onChange" | "value"
|
|
47
|
-
> {
|
|
48
|
-
field: string;
|
|
49
|
-
label: string;
|
|
50
|
-
value: ActiveFilterValue | undefined;
|
|
51
|
-
onChange: (value: ActiveFilterValue | undefined) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function TextFilterInput({
|
|
55
|
-
field,
|
|
56
|
-
label,
|
|
57
|
-
value,
|
|
58
|
-
onChange,
|
|
59
|
-
className,
|
|
60
|
-
...props
|
|
61
|
-
}: TextFilterInputProps) {
|
|
62
|
-
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
63
|
-
|
|
64
|
-
const externalValue = value?.value ?? "";
|
|
65
|
-
const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
|
|
66
|
-
if (prevExternalValue !== externalValue) {
|
|
67
|
-
setPrevExternalValue(externalValue);
|
|
68
|
-
setLocalValue(externalValue);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
72
|
-
if (v) {
|
|
73
|
-
onChange({ field, label, type: "text", value: v });
|
|
74
|
-
} else {
|
|
75
|
-
onChange(undefined);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<Input
|
|
81
|
-
id={`filter-${field}`}
|
|
82
|
-
type="text"
|
|
83
|
-
placeholder={props.placeholder ?? `Filter by ${label.toLowerCase()}...`}
|
|
84
|
-
value={localValue}
|
|
85
|
-
onChange={(e) => {
|
|
86
|
-
setLocalValue(e.target.value);
|
|
87
|
-
debouncedOnChange(e.target.value);
|
|
88
|
-
}}
|
|
89
|
-
className={cn(className)}
|
|
90
|
-
{...props}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
interface UseAsyncDataResult<T> {
|
|
4
|
-
data: T | null;
|
|
5
|
-
loading: boolean;
|
|
6
|
-
error: string | null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Runs an async fetcher on mount and whenever `deps` change.
|
|
11
|
-
* Returns the loading/error/data state. Does not cache — every call
|
|
12
|
-
* to the fetcher hits the source directly.
|
|
13
|
-
*
|
|
14
|
-
* A cleanup flag prevents state updates if the component unmounts
|
|
15
|
-
* or deps change before the fetch completes (avoids React warnings
|
|
16
|
-
* and stale updates from out-of-order responses).
|
|
17
|
-
*/
|
|
18
|
-
export function useAsyncData<T>(
|
|
19
|
-
fetcher: () => Promise<T>,
|
|
20
|
-
deps: React.DependencyList,
|
|
21
|
-
): UseAsyncDataResult<T> {
|
|
22
|
-
const [data, setData] = useState<T | null>(null);
|
|
23
|
-
const [loading, setLoading] = useState(true);
|
|
24
|
-
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
const [generation, setGeneration] = useState(0);
|
|
26
|
-
|
|
27
|
-
const fetcherRef = useRef(fetcher);
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
fetcherRef.current = fetcher;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Detect dep changes during render to reset loading state and bump generation
|
|
33
|
-
const [prevDeps, setPrevDeps] = useState(deps);
|
|
34
|
-
if (deps.length !== prevDeps.length || deps.some((d, i) => d !== prevDeps[i])) {
|
|
35
|
-
setPrevDeps(deps);
|
|
36
|
-
setGeneration((g) => g + 1);
|
|
37
|
-
if (!loading) setLoading(true);
|
|
38
|
-
if (error !== null) setError(null);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
let cancelled = false;
|
|
43
|
-
|
|
44
|
-
fetcherRef
|
|
45
|
-
.current()
|
|
46
|
-
.then((result) => {
|
|
47
|
-
if (!cancelled) setData(result);
|
|
48
|
-
})
|
|
49
|
-
.catch((err) => {
|
|
50
|
-
console.error(err);
|
|
51
|
-
if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
|
|
52
|
-
})
|
|
53
|
-
.finally(() => {
|
|
54
|
-
if (!cancelled) setLoading(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
cancelled = true;
|
|
59
|
-
};
|
|
60
|
-
}, [generation]);
|
|
61
|
-
|
|
62
|
-
return { data, loading, error };
|
|
63
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import { debounce, FILTER_DEBOUNCE_MS } from "../utils/debounce";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Returns a stable debounced wrapper around the provided callback.
|
|
6
|
-
*
|
|
7
|
-
* The wrapper always invokes the *latest* version of `fn` (via a ref),
|
|
8
|
-
* so the debounce timer is never reset when `fn` changes — only when
|
|
9
|
-
* the caller invokes the returned function again.
|
|
10
|
-
*
|
|
11
|
-
* @param fn - The callback to debounce.
|
|
12
|
-
* @param delay - Debounce delay in ms. Defaults to `FILTER_DEBOUNCE_MS`.
|
|
13
|
-
*/
|
|
14
|
-
export function useDebouncedCallback<T extends (...args: any[]) => void>(
|
|
15
|
-
fn: T,
|
|
16
|
-
delay: number = FILTER_DEBOUNCE_MS,
|
|
17
|
-
): (...args: Parameters<T>) => void {
|
|
18
|
-
const fnRef = useRef(fn);
|
|
19
|
-
const debouncedRef = useRef<((...args: any[]) => void) | null>(null);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
fnRef.current = fn;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
debouncedRef.current = debounce((...args: any[]) => {
|
|
27
|
-
fnRef.current(...(args as Parameters<T>));
|
|
28
|
-
}, delay);
|
|
29
|
-
}, [delay]);
|
|
30
|
-
|
|
31
|
-
return useCallback((...args: Parameters<T>) => {
|
|
32
|
-
debouncedRef.current?.(...args);
|
|
33
|
-
}, []);
|
|
34
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export function fieldValue(
|
|
2
|
-
field: { displayValue?: string | null; value?: unknown } | null | undefined,
|
|
3
|
-
): string | null {
|
|
4
|
-
if (field?.displayValue != null) return field.displayValue;
|
|
5
|
-
if (field?.value != null) return String(field.value);
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getAddressFieldLines(address: {
|
|
10
|
-
street?: string | null;
|
|
11
|
-
city?: string | null;
|
|
12
|
-
state?: string | null;
|
|
13
|
-
postalCode?: string | null;
|
|
14
|
-
country?: string | null;
|
|
15
|
-
}) {
|
|
16
|
-
const cityStateZip = [address.city, address.state].filter(Boolean).join(", ");
|
|
17
|
-
const cityStateZipLine = [cityStateZip, address.postalCode].filter(Boolean).join(" ");
|
|
18
|
-
const lines = [address.street, cityStateZipLine, address.country].filter(Boolean);
|
|
19
|
-
if (lines.length === 0) return null;
|
|
20
|
-
return lines;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function formatDateTimeField(
|
|
24
|
-
value?: string | null,
|
|
25
|
-
...args: Parameters<Date["toLocaleString"]>
|
|
26
|
-
) {
|
|
27
|
-
if (!value) return null;
|
|
28
|
-
return new Date(value).toLocaleString(...args);
|
|
29
|
-
}
|