@salesforce/webapp-template-app-react-sample-b2e-experimental 1.117.0 → 1.117.1
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 +8 -0
- package/dist/force-app/main/default/webapplications/propertymanagementapp/package.json +3 -3
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/components/layout/FilterRow.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +15 -6
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/FilterContext.tsx +1 -32
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/BooleanFilter.tsx +9 -5
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/DateFilter.tsx +15 -8
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/DateRangeFilter.tsx +8 -7
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +33 -0
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +4 -5
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +113 -82
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/SearchFilter.tsx +24 -11
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/SelectFilter.tsx +9 -5
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/components/filters/TextFilter.tsx +29 -12
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/hooks/useDebouncedCallback.ts +34 -0
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/hooks/useObjectSearchParams.ts +5 -86
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/utils/debounce.ts +4 -1
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/features/object-search/utils/filterUtils.ts +24 -1
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/pages/ApplicationSearch.tsx +13 -18
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/pages/MaintenanceRequestSearch.tsx +13 -18
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/pages/MaintenanceWorkerSearch.tsx +11 -18
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/pages/PropertySearch.tsx +6 -15
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/webapplications/propertymanagementapp/src/lib/filterUtils.ts +0 -11
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
3
|
import { SearchBar } from "../SearchBar";
|
|
4
4
|
import { useFilterField } from "../FilterContext";
|
|
5
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
6
|
+
import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
|
|
5
7
|
|
|
6
8
|
interface SearchFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
7
9
|
field: string;
|
|
@@ -17,21 +19,32 @@ export function SearchFilter({
|
|
|
17
19
|
...props
|
|
18
20
|
}: SearchFilterProps) {
|
|
19
21
|
const { value, onChange } = useFilterField(field);
|
|
22
|
+
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
23
|
+
|
|
24
|
+
const externalValue = value?.value ?? "";
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setLocalValue(externalValue);
|
|
27
|
+
}, [externalValue]);
|
|
28
|
+
|
|
29
|
+
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
30
|
+
if (v) {
|
|
31
|
+
onChange({ field, label, type: "search", value: v });
|
|
32
|
+
} else {
|
|
33
|
+
onChange(undefined);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
20
37
|
return (
|
|
21
|
-
<
|
|
22
|
-
<Label htmlFor={`filter-${field}`}>{label}</Label>
|
|
38
|
+
<FilterFieldWrapper label={label} htmlFor={`filter-${field}`} className={className} {...props}>
|
|
23
39
|
<SearchBar
|
|
24
|
-
value={
|
|
40
|
+
value={localValue}
|
|
25
41
|
handleChange={(v) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
onChange(undefined);
|
|
30
|
-
}
|
|
42
|
+
setLocalValue(v);
|
|
43
|
+
debouncedOnChange(v);
|
|
31
44
|
}}
|
|
32
45
|
placeholder={placeholder}
|
|
33
46
|
inputProps={{ id: `filter-${field}` }}
|
|
34
47
|
/>
|
|
35
|
-
</
|
|
48
|
+
</FilterFieldWrapper>
|
|
36
49
|
);
|
|
37
50
|
}
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
SelectTrigger,
|
|
6
6
|
SelectValue,
|
|
7
7
|
} from "../../../../components/ui/select";
|
|
8
|
-
import { Label } from "../../../../components/ui/label";
|
|
9
8
|
import { cn } from "../../../../lib/utils";
|
|
10
9
|
import { useFilterField } from "../FilterContext";
|
|
10
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
11
11
|
import type { ActiveFilterValue } from "../../utils/filterUtils";
|
|
12
12
|
|
|
13
13
|
const ALL_VALUE = "__all__";
|
|
@@ -29,8 +29,13 @@ export function SelectFilter({
|
|
|
29
29
|
}: SelectFilterProps) {
|
|
30
30
|
const { value, onChange } = useFilterField(field);
|
|
31
31
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
32
|
+
<FilterFieldWrapper
|
|
33
|
+
label={label}
|
|
34
|
+
htmlFor={`filter-${field}`}
|
|
35
|
+
helpText={helpText}
|
|
36
|
+
className={className}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
34
39
|
<SelectFilterControl
|
|
35
40
|
field={field}
|
|
36
41
|
label={label}
|
|
@@ -38,8 +43,7 @@ export function SelectFilter({
|
|
|
38
43
|
value={value}
|
|
39
44
|
onChange={onChange}
|
|
40
45
|
/>
|
|
41
|
-
|
|
42
|
-
</div>
|
|
46
|
+
</FilterFieldWrapper>
|
|
43
47
|
);
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import { Input } from "../../../../components/ui/input";
|
|
2
|
-
import { Label } from "../../../../components/ui/label";
|
|
3
3
|
import { cn } from "../../../../lib/utils";
|
|
4
4
|
import { useFilterField } from "../FilterContext";
|
|
5
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
6
|
+
import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
|
|
5
7
|
import type { ActiveFilterValue } from "../../utils/filterUtils";
|
|
6
8
|
|
|
7
9
|
interface TextFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
|
|
@@ -21,8 +23,13 @@ export function TextFilter({
|
|
|
21
23
|
}: TextFilterProps) {
|
|
22
24
|
const { value, onChange } = useFilterField(field);
|
|
23
25
|
return (
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
+
<FilterFieldWrapper
|
|
27
|
+
label={label}
|
|
28
|
+
htmlFor={`filter-${field}`}
|
|
29
|
+
helpText={helpText}
|
|
30
|
+
className={className}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
26
33
|
<TextFilterInput
|
|
27
34
|
field={field}
|
|
28
35
|
label={label}
|
|
@@ -30,8 +37,7 @@ export function TextFilter({
|
|
|
30
37
|
value={value}
|
|
31
38
|
onChange={onChange}
|
|
32
39
|
/>
|
|
33
|
-
|
|
34
|
-
</div>
|
|
40
|
+
</FilterFieldWrapper>
|
|
35
41
|
);
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -53,19 +59,30 @@ export function TextFilterInput({
|
|
|
53
59
|
className,
|
|
54
60
|
...props
|
|
55
61
|
}: TextFilterInputProps) {
|
|
62
|
+
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
63
|
+
|
|
64
|
+
const externalValue = value?.value ?? "";
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setLocalValue(externalValue);
|
|
67
|
+
}, [externalValue]);
|
|
68
|
+
|
|
69
|
+
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
70
|
+
if (v) {
|
|
71
|
+
onChange({ field, label, type: "text", value: v });
|
|
72
|
+
} else {
|
|
73
|
+
onChange(undefined);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
56
77
|
return (
|
|
57
78
|
<Input
|
|
58
79
|
id={`filter-${field}`}
|
|
59
80
|
type="text"
|
|
60
81
|
placeholder={props.placeholder ?? `Filter by ${label.toLowerCase()}...`}
|
|
61
|
-
value={
|
|
82
|
+
value={localValue}
|
|
62
83
|
onChange={(e) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
onChange({ field, label, type: "text", value: v });
|
|
66
|
-
} else {
|
|
67
|
-
onChange(undefined);
|
|
68
|
-
}
|
|
84
|
+
setLocalValue(e.target.value);
|
|
85
|
+
debouncedOnChange(e.target.value);
|
|
69
86
|
}}
|
|
70
87
|
className={cn(className)}
|
|
71
88
|
{...props}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
@@ -20,11 +20,6 @@ export interface UseObjectSearchParamsReturn<TFilter, TOrderBy> {
|
|
|
20
20
|
set: (field: string, value: ActiveFilterValue | undefined) => void;
|
|
21
21
|
remove: (field: string) => void;
|
|
22
22
|
};
|
|
23
|
-
filterState: {
|
|
24
|
-
apply: () => void;
|
|
25
|
-
hasPendingChanges: boolean;
|
|
26
|
-
hasValidationError: boolean;
|
|
27
|
-
};
|
|
28
23
|
sort: {
|
|
29
24
|
current: SortState | null;
|
|
30
25
|
set: (sort: SortState | null) => void;
|
|
@@ -41,40 +36,6 @@ export interface UseObjectSearchParamsReturn<TFilter, TOrderBy> {
|
|
|
41
36
|
resetAll: () => void;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
export interface UseObjectSearchParamsOptions {
|
|
45
|
-
filterSyncMode?: "immediate" | "manual";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function areFiltersEqual(left: ActiveFilterValue[], right: ActiveFilterValue[]) {
|
|
49
|
-
if (left.length !== right.length) return false;
|
|
50
|
-
const normalize = (filters: ActiveFilterValue[]) =>
|
|
51
|
-
[...filters]
|
|
52
|
-
.sort((a, b) => a.field.localeCompare(b.field))
|
|
53
|
-
.map((filter) => ({
|
|
54
|
-
field: filter.field,
|
|
55
|
-
type: filter.type,
|
|
56
|
-
value: filter.value ?? "",
|
|
57
|
-
min: filter.min ?? "",
|
|
58
|
-
max: filter.max ?? "",
|
|
59
|
-
}));
|
|
60
|
-
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function hasFilterValidationError(filters: ActiveFilterValue[]) {
|
|
64
|
-
for (const filter of filters) {
|
|
65
|
-
if (filter.type !== "numeric") continue;
|
|
66
|
-
const min = filter.min?.trim();
|
|
67
|
-
const max = filter.max?.trim();
|
|
68
|
-
if (!min || !max) continue;
|
|
69
|
-
const minValue = Number(min);
|
|
70
|
-
const maxValue = Number(max);
|
|
71
|
-
if (!Number.isNaN(minValue) && !Number.isNaN(maxValue) && minValue >= maxValue) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
39
|
/**
|
|
79
40
|
* Manages filter, sort, and cursor-based pagination state for an object search page.
|
|
80
41
|
*
|
|
@@ -98,11 +59,7 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
98
59
|
filterConfigs: FilterFieldConfig[],
|
|
99
60
|
_sortConfigs?: SortFieldConfig[],
|
|
100
61
|
paginationConfig?: PaginationConfig,
|
|
101
|
-
options?: UseObjectSearchParamsOptions,
|
|
102
62
|
) {
|
|
103
|
-
const filterSyncMode = options?.filterSyncMode ?? "immediate";
|
|
104
|
-
const isManualFilterSync = filterSyncMode === "manual";
|
|
105
|
-
|
|
106
63
|
const defaultPageSize = paginationConfig?.defaultPageSize ?? 10;
|
|
107
64
|
const validPageSizes = useMemo(
|
|
108
65
|
() => paginationConfig?.validPageSizes ?? [defaultPageSize],
|
|
@@ -119,7 +76,6 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
119
76
|
);
|
|
120
77
|
|
|
121
78
|
const [filters, setFilters] = useState<ActiveFilterValue[]>(initial.filters);
|
|
122
|
-
const [appliedFilters, setAppliedFilters] = useState<ActiveFilterValue[]>(initial.filters);
|
|
123
79
|
const [sort, setLocalSort] = useState<SortState | null>(initial.sort);
|
|
124
80
|
|
|
125
81
|
// Pagination — cursor-based with a stack to support "previous page" navigation.
|
|
@@ -170,15 +126,6 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
170
126
|
|
|
171
127
|
const setFilter = useCallback(
|
|
172
128
|
(field: string, value: ActiveFilterValue | undefined) => {
|
|
173
|
-
if (isManualFilterSync) {
|
|
174
|
-
setFilters((prev) => {
|
|
175
|
-
const next = prev.filter((f) => f.field !== field);
|
|
176
|
-
if (value) next.push(value);
|
|
177
|
-
return next;
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
129
|
const { sort: s, pageSize: ps } = stateRef.current;
|
|
183
130
|
setFilters((prev) => {
|
|
184
131
|
const next = prev.filter((f) => f.field !== field);
|
|
@@ -188,16 +135,11 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
188
135
|
});
|
|
189
136
|
resetPagination();
|
|
190
137
|
},
|
|
191
|
-
[
|
|
138
|
+
[resetPagination],
|
|
192
139
|
);
|
|
193
140
|
|
|
194
141
|
const removeFilter = useCallback(
|
|
195
142
|
(field: string) => {
|
|
196
|
-
if (isManualFilterSync) {
|
|
197
|
-
setFilters((prev) => prev.filter((f) => f.field !== field));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
143
|
const { sort: s, pageSize: ps } = stateRef.current;
|
|
202
144
|
setFilters((prev) => {
|
|
203
145
|
const next = prev.filter((f) => f.field !== field);
|
|
@@ -206,17 +148,9 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
206
148
|
});
|
|
207
149
|
resetPagination();
|
|
208
150
|
},
|
|
209
|
-
[
|
|
151
|
+
[resetPagination],
|
|
210
152
|
);
|
|
211
153
|
|
|
212
|
-
const applyFilters = useCallback(() => {
|
|
213
|
-
if (!isManualFilterSync) return;
|
|
214
|
-
const { filters: nextFilters, sort: s, pageSize: ps } = stateRef.current;
|
|
215
|
-
setAppliedFilters(nextFilters);
|
|
216
|
-
resetPagination();
|
|
217
|
-
syncToUrl(nextFilters, s, ps);
|
|
218
|
-
}, [isManualFilterSync, resetPagination, syncToUrl]);
|
|
219
|
-
|
|
220
154
|
// -- Sort callback ----------------------------------------------------------
|
|
221
155
|
|
|
222
156
|
const setSort = useCallback(
|
|
@@ -233,7 +167,6 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
233
167
|
|
|
234
168
|
const resetAll = useCallback(() => {
|
|
235
169
|
setFilters([]);
|
|
236
|
-
setAppliedFilters([]);
|
|
237
170
|
setLocalSort(null);
|
|
238
171
|
resetPagination();
|
|
239
172
|
syncToUrl([], null, defaultPageSize, 0);
|
|
@@ -283,8 +216,8 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
283
216
|
// Translate local filter/sort state into API-ready `where` and `orderBy`.
|
|
284
217
|
|
|
285
218
|
const where = useMemo(
|
|
286
|
-
() => buildFilter<TFilter>(
|
|
287
|
-
[
|
|
219
|
+
() => buildFilter<TFilter>(filters, filterConfigs),
|
|
220
|
+
[filters, filterConfigs],
|
|
288
221
|
);
|
|
289
222
|
|
|
290
223
|
const orderBy = useMemo(() => buildOrderBy<TOrderBy>(sort), [sort]);
|
|
@@ -296,23 +229,10 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
296
229
|
// causing unnecessary re-renders.
|
|
297
230
|
|
|
298
231
|
const filtersGroup = useMemo(
|
|
299
|
-
() => ({
|
|
300
|
-
active: filters,
|
|
301
|
-
set: setFilter,
|
|
302
|
-
remove: removeFilter,
|
|
303
|
-
}),
|
|
232
|
+
() => ({ active: filters, set: setFilter, remove: removeFilter }),
|
|
304
233
|
[filters, setFilter, removeFilter],
|
|
305
234
|
);
|
|
306
235
|
|
|
307
|
-
const filterState = useMemo(
|
|
308
|
-
() => ({
|
|
309
|
-
apply: applyFilters,
|
|
310
|
-
hasPendingChanges: isManualFilterSync ? !areFiltersEqual(filters, appliedFilters) : false,
|
|
311
|
-
hasValidationError: hasFilterValidationError(filters),
|
|
312
|
-
}),
|
|
313
|
-
[applyFilters, isManualFilterSync, filters, appliedFilters],
|
|
314
|
-
);
|
|
315
|
-
|
|
316
236
|
const sortGroup = useMemo(() => ({ current: sort, set: setSort }), [sort, setSort]);
|
|
317
237
|
|
|
318
238
|
const query = useMemo(() => ({ where, orderBy }), [where, orderBy]);
|
|
@@ -324,7 +244,6 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
324
244
|
|
|
325
245
|
return {
|
|
326
246
|
filters: filtersGroup,
|
|
327
|
-
filterState,
|
|
328
247
|
sort: sortGroup,
|
|
329
248
|
query,
|
|
330
249
|
pagination,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Default debounce delay for keystroke-driven filter inputs (search, text, numeric). */
|
|
2
|
+
export const FILTER_DEBOUNCE_MS = 300;
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Creates a debounced version of the provided function.
|
|
3
6
|
*
|
|
@@ -10,7 +13,7 @@
|
|
|
10
13
|
* @param ms - The debounce delay in milliseconds.
|
|
11
14
|
* @returns A new function with the same signature that delays execution.
|
|
12
15
|
*/
|
|
13
|
-
export function debounce<T extends (...args:
|
|
16
|
+
export function debounce<T extends (...args: any[]) => void>(
|
|
14
17
|
fn: T,
|
|
15
18
|
ms: number,
|
|
16
19
|
): (...args: Parameters<T>) => void {
|
|
@@ -25,6 +25,8 @@ export type FilterFieldType =
|
|
|
25
25
|
| "boolean"
|
|
26
26
|
| "date"
|
|
27
27
|
| "daterange"
|
|
28
|
+
| "datetime"
|
|
29
|
+
| "datetimerange"
|
|
28
30
|
| "multipicklist"
|
|
29
31
|
| "search";
|
|
30
32
|
|
|
@@ -337,13 +339,34 @@ function buildSingleFilter<TFilter>(
|
|
|
337
339
|
return { [field]: { in: values } } as TFilter;
|
|
338
340
|
}
|
|
339
341
|
case "date": {
|
|
342
|
+
if (!min && !max) return null;
|
|
343
|
+
const op = value ?? (min ? "gte" : "lte");
|
|
344
|
+
const dateStr = min ?? max;
|
|
345
|
+
return { [field]: { [op]: { value: dateStr } } } as TFilter;
|
|
346
|
+
}
|
|
347
|
+
case "daterange": {
|
|
348
|
+
if (!min && !max) return null;
|
|
349
|
+
const clauses: TFilter[] = [];
|
|
350
|
+
if (min) {
|
|
351
|
+
clauses.push({
|
|
352
|
+
[field]: { gte: { value: min } },
|
|
353
|
+
} as TFilter);
|
|
354
|
+
}
|
|
355
|
+
if (max) {
|
|
356
|
+
clauses.push({
|
|
357
|
+
[field]: { lte: { value: max } },
|
|
358
|
+
} as TFilter);
|
|
359
|
+
}
|
|
360
|
+
return clauses.length === 1 ? clauses[0] : ({ and: clauses } as TFilter);
|
|
361
|
+
}
|
|
362
|
+
case "datetime": {
|
|
340
363
|
if (!min && !max) return null;
|
|
341
364
|
const op = value ?? (min ? "gte" : "lte");
|
|
342
365
|
const dateStr = min ?? max;
|
|
343
366
|
const isoStr = op === "gte" || op === "gt" ? toStartOfDay(dateStr!) : toEndOfDay(dateStr!);
|
|
344
367
|
return { [field]: { [op]: { value: isoStr } } } as TFilter;
|
|
345
368
|
}
|
|
346
|
-
case "
|
|
369
|
+
case "datetimerange": {
|
|
347
370
|
if (!min && !max) return null;
|
|
348
371
|
const clauses: TFilter[] = [];
|
|
349
372
|
if (min) {
|
|
@@ -16,7 +16,6 @@ import type { ApplicationSearchNode } from "../api/applications/applicationSearc
|
|
|
16
16
|
import { PageHeader } from "../components/layout/PageHeader";
|
|
17
17
|
import { PageContainer } from "../components/layout/PageContainer";
|
|
18
18
|
import {
|
|
19
|
-
FilterApplyButton,
|
|
20
19
|
FilterProvider,
|
|
21
20
|
FilterResetButton,
|
|
22
21
|
} from "../features/object-search/components/FilterContext";
|
|
@@ -56,7 +55,7 @@ const FILTER_CONFIGS: FilterFieldConfig[] = [
|
|
|
56
55
|
},
|
|
57
56
|
{ field: "Status__c", label: "Status", type: "picklist" },
|
|
58
57
|
{ field: "Start_Date__c", label: "Start Date", type: "date" },
|
|
59
|
-
{ field: "CreatedDate", label: "Created Date", type: "
|
|
58
|
+
{ field: "CreatedDate", label: "Created Date", type: "datetime" },
|
|
60
59
|
];
|
|
61
60
|
|
|
62
61
|
const SORT_CONFIGS: SortFieldConfig<string>[] = [
|
|
@@ -76,13 +75,14 @@ export default function ApplicationSearch() {
|
|
|
76
75
|
ttl: 30_000,
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
const { filters,
|
|
78
|
+
const { filters, query, pagination, resetAll } = useObjectSearchParams<
|
|
80
79
|
Application__C_Filter,
|
|
81
80
|
Application__C_OrderBy
|
|
82
|
-
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG
|
|
83
|
-
const effectiveOrderBy
|
|
84
|
-
CreatedDate: { order: ResultOrder.Desc },
|
|
85
|
-
|
|
81
|
+
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG);
|
|
82
|
+
const effectiveOrderBy = useMemo<Application__C_OrderBy>(
|
|
83
|
+
() => query.orderBy ?? { CreatedDate: { order: ResultOrder.Desc } },
|
|
84
|
+
[query.orderBy],
|
|
85
|
+
);
|
|
86
86
|
const searchKey = `applications:${JSON.stringify({ where: query.where, orderBy: effectiveOrderBy, first: pagination.pageSize, after: pagination.afterCursor })}`;
|
|
87
87
|
const { data, loading, error } = useCachedAsyncData(
|
|
88
88
|
() =>
|
|
@@ -138,7 +138,6 @@ export default function ApplicationSearch() {
|
|
|
138
138
|
<PageHeader title="Applications" description="Manage and review rental applications" />
|
|
139
139
|
<ApplicationSearchFilters
|
|
140
140
|
filters={filters}
|
|
141
|
-
filterState={filterState}
|
|
142
141
|
statusOptions={statusOptions ?? []}
|
|
143
142
|
resetAll={resetAll}
|
|
144
143
|
/>
|
|
@@ -193,15 +192,10 @@ export default function ApplicationSearch() {
|
|
|
193
192
|
|
|
194
193
|
function ApplicationSearchFilters({
|
|
195
194
|
filters,
|
|
196
|
-
filterState,
|
|
197
195
|
statusOptions,
|
|
198
196
|
resetAll,
|
|
199
197
|
}: {
|
|
200
198
|
filters: UseObjectSearchParamsReturn<Application__C_Filter, Application__C_OrderBy>["filters"];
|
|
201
|
-
filterState: UseObjectSearchParamsReturn<
|
|
202
|
-
Application__C_Filter,
|
|
203
|
-
Application__C_OrderBy
|
|
204
|
-
>["filterState"];
|
|
205
199
|
statusOptions: Array<{ value: string; label: string }>;
|
|
206
200
|
resetAll: () => void;
|
|
207
201
|
}) {
|
|
@@ -211,9 +205,6 @@ function ApplicationSearchFilters({
|
|
|
211
205
|
onFilterChange={filters.set}
|
|
212
206
|
onFilterRemove={filters.remove}
|
|
213
207
|
onReset={resetAll}
|
|
214
|
-
onApply={filterState.apply}
|
|
215
|
-
hasPendingChanges={filterState.hasPendingChanges}
|
|
216
|
-
hasValidationError={filterState.hasValidationError}
|
|
217
208
|
>
|
|
218
209
|
<FilterRow ariaLabel="Applications filters">
|
|
219
210
|
<SearchFilter
|
|
@@ -229,8 +220,12 @@ function ApplicationSearchFilters({
|
|
|
229
220
|
className="w-full sm:w-36"
|
|
230
221
|
/>
|
|
231
222
|
<DateFilter field="Start_Date__c" label="Start Date" className="w-full sm:w-56" />
|
|
232
|
-
<DateFilter
|
|
233
|
-
|
|
223
|
+
<DateFilter
|
|
224
|
+
field="CreatedDate"
|
|
225
|
+
label="Created Date"
|
|
226
|
+
filterType="datetime"
|
|
227
|
+
className="w-full sm:w-56"
|
|
228
|
+
/>
|
|
234
229
|
<FilterResetButton />
|
|
235
230
|
</FilterRow>
|
|
236
231
|
</FilterProvider>
|
|
@@ -18,7 +18,6 @@ import type { SortFieldConfig } from "../features/object-search/utils/sortUtils"
|
|
|
18
18
|
import { PageHeader } from "../components/layout/PageHeader";
|
|
19
19
|
import { PageContainer } from "../components/layout/PageContainer";
|
|
20
20
|
import {
|
|
21
|
-
FilterApplyButton,
|
|
22
21
|
FilterProvider,
|
|
23
22
|
FilterResetButton,
|
|
24
23
|
} from "../features/object-search/components/FilterContext";
|
|
@@ -74,7 +73,7 @@ const FILTER_CONFIGS: FilterFieldConfig[] = [
|
|
|
74
73
|
{ field: "Status__c", label: "Status", type: "picklist" },
|
|
75
74
|
{ field: "Type__c", label: "Type", type: "picklist" },
|
|
76
75
|
{ field: "Priority__c", label: "Priority", type: "picklist" },
|
|
77
|
-
{ field: "Scheduled__c", label: "Scheduled", type: "
|
|
76
|
+
{ field: "Scheduled__c", label: "Scheduled", type: "datetime" },
|
|
78
77
|
];
|
|
79
78
|
|
|
80
79
|
const SORT_CONFIGS: SortFieldConfig<string>[] = [
|
|
@@ -102,13 +101,14 @@ export default function MaintenanceRequestSearch() {
|
|
|
102
101
|
{ key: "distinctMaintenanceRequestPriority", ttl: 30_000 },
|
|
103
102
|
);
|
|
104
103
|
|
|
105
|
-
const { filters,
|
|
104
|
+
const { filters, query, pagination, resetAll } = useObjectSearchParams<
|
|
106
105
|
Maintenance_Request__C_Filter,
|
|
107
106
|
Maintenance_Request__C_OrderBy
|
|
108
|
-
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG
|
|
109
|
-
const effectiveOrderBy
|
|
110
|
-
CreatedDate: { order: ResultOrder.Desc },
|
|
111
|
-
|
|
107
|
+
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG);
|
|
108
|
+
const effectiveOrderBy = useMemo<Maintenance_Request__C_OrderBy>(
|
|
109
|
+
() => query.orderBy ?? { CreatedDate: { order: ResultOrder.Desc } },
|
|
110
|
+
[query.orderBy],
|
|
111
|
+
);
|
|
112
112
|
const searchKey = `maintenance-requests:${JSON.stringify({ where: query.where, orderBy: effectiveOrderBy, first: pagination.pageSize, after: pagination.afterCursor })}`;
|
|
113
113
|
const { data, loading, error } = useCachedAsyncData(
|
|
114
114
|
() =>
|
|
@@ -167,7 +167,6 @@ export default function MaintenanceRequestSearch() {
|
|
|
167
167
|
/>
|
|
168
168
|
<MaintenanceRequestSearchFilters
|
|
169
169
|
filters={filters}
|
|
170
|
-
filterState={filterState}
|
|
171
170
|
statusOptions={statusOptions ?? []}
|
|
172
171
|
typeOptions={typeOptions ?? []}
|
|
173
172
|
priorityOptions={priorityOptions ?? []}
|
|
@@ -225,7 +224,6 @@ export default function MaintenanceRequestSearch() {
|
|
|
225
224
|
|
|
226
225
|
function MaintenanceRequestSearchFilters({
|
|
227
226
|
filters,
|
|
228
|
-
filterState,
|
|
229
227
|
statusOptions,
|
|
230
228
|
typeOptions,
|
|
231
229
|
priorityOptions,
|
|
@@ -235,10 +233,6 @@ function MaintenanceRequestSearchFilters({
|
|
|
235
233
|
Maintenance_Request__C_Filter,
|
|
236
234
|
Maintenance_Request__C_OrderBy
|
|
237
235
|
>["filters"];
|
|
238
|
-
filterState: UseObjectSearchParamsReturn<
|
|
239
|
-
Maintenance_Request__C_Filter,
|
|
240
|
-
Maintenance_Request__C_OrderBy
|
|
241
|
-
>["filterState"];
|
|
242
236
|
statusOptions: Array<{ value: string; label: string }>;
|
|
243
237
|
typeOptions: Array<{ value: string; label: string }>;
|
|
244
238
|
priorityOptions: Array<{ value: string; label: string }>;
|
|
@@ -250,9 +244,6 @@ function MaintenanceRequestSearchFilters({
|
|
|
250
244
|
onFilterChange={filters.set}
|
|
251
245
|
onFilterRemove={filters.remove}
|
|
252
246
|
onReset={resetAll}
|
|
253
|
-
onApply={filterState.apply}
|
|
254
|
-
hasPendingChanges={filterState.hasPendingChanges}
|
|
255
|
-
hasValidationError={filterState.hasValidationError}
|
|
256
247
|
>
|
|
257
248
|
<FilterRow ariaLabel="Maintenance Requests filters">
|
|
258
249
|
<SearchFilter
|
|
@@ -279,8 +270,12 @@ function MaintenanceRequestSearchFilters({
|
|
|
279
270
|
options={priorityOptions}
|
|
280
271
|
className="w-full sm:w-36"
|
|
281
272
|
/>
|
|
282
|
-
<DateFilter
|
|
283
|
-
|
|
273
|
+
<DateFilter
|
|
274
|
+
field="Scheduled__c"
|
|
275
|
+
label="Scheduled"
|
|
276
|
+
filterType="datetime"
|
|
277
|
+
className="w-full sm:w-56"
|
|
278
|
+
/>
|
|
284
279
|
<FilterResetButton />
|
|
285
280
|
</FilterRow>
|
|
286
281
|
</FilterProvider>
|