@salesforce/ui-bundle-template-app-react-sample-b2e 11.20.0 → 11.22.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.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/applicationSearchService.ts +25 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/maintenanceRequestSearchService.ts +31 -65
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/maintenanceWorkerSearchService.ts +21 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/propertySearchService.ts +33 -53
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/applications/ApplicationCard.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/dashboard/GlobalSearchBar.tsx +2 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/layout/VerticalNav.tsx +28 -18
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceRequests/MaintenanceRequestCard.tsx +79 -52
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceWorkers/WorkerCard.tsx +87 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/search/UnifiedSearch.tsx +213 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/distinctValuesService.ts +84 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/searchService.ts +71 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/MergedSearchResults.tsx +263 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/Search.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SearchResults.tsx +77 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SourceSection.tsx +166 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SortControl.tsx +62 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/config.json +83 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useSearch.ts +703 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/index.ts +90 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/loadConfig.ts +14 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/queryBuilder.ts +156 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/types.ts +307 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/debounce.ts +17 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/fieldUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/filterUtils.ts +285 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/sortUtils.ts +34 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/constants.ts +16 -29
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/routeConfig.ts +1 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/searchConfig.ts +190 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/ApplicationSearch.tsx +8 -385
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Home.tsx +4 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceRequestSearch.tsx +8 -458
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceWorkerSearch.tsx +8 -341
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/PropertySearch.tsx +7 -243
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Search.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/routes.tsx +6 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/distinctApplicationStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/searchApplications.graphql +0 -65
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestPriority.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/searchMaintenanceRequests.graphql +0 -82
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/distinctMaintenanceWorkerEmploymentType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/searchMaintenanceWorkers.graphql +0 -64
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/searchProperties.graphql +0 -105
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/ObjectSearchErrorState.tsx +0 -45
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/UserAvatar.tsx +0 -35
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/api/objectSearchService.ts +0 -82
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/PaginationControls.tsx +0 -109
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SearchBar.tsx +0 -41
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SortControl.tsx +0 -143
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/debounce.ts +0 -25
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/fieldUtils.ts +0 -29
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/filterUtils.ts +0 -404
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/sortUtils.ts +0 -38
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/filterUtils.ts +0 -11
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Input } from "../../../../../components/ui/input";
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "../../../../../components/ui/select";
|
|
10
|
+
import { useFilterField } from "../FilterContext";
|
|
11
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
12
|
+
import type {
|
|
13
|
+
ActiveFilterValue,
|
|
14
|
+
DateFilterMode,
|
|
15
|
+
FilterFieldType,
|
|
16
|
+
} from "../../../utils/filterUtils";
|
|
17
|
+
|
|
18
|
+
interface DateRangeFilterProps {
|
|
19
|
+
field: string;
|
|
20
|
+
label: string;
|
|
21
|
+
helpText?: string;
|
|
22
|
+
/** "daterange" (Date scalar) or "datetimerange" (DateTime scalar). */
|
|
23
|
+
filterType?: Extract<FilterFieldType, "daterange" | "datetimerange">;
|
|
24
|
+
/**
|
|
25
|
+
* Which comparison chrome to show. See {@link DateFilterMode}. Defaults to
|
|
26
|
+
* `"comparison"` (an After / Before selector with one date input).
|
|
27
|
+
*/
|
|
28
|
+
mode?: DateFilterMode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The three ways to constrain a date field. They map directly onto the
|
|
33
|
+
* `min`/`max` bounds the query builder already understands:
|
|
34
|
+
* - after → only `min` set → `gte`
|
|
35
|
+
* - before → only `max` set → `lte`
|
|
36
|
+
* - between → both set → `gte` AND `lte`
|
|
37
|
+
*/
|
|
38
|
+
type DateOperator = "between" | "after" | "before";
|
|
39
|
+
|
|
40
|
+
const OPERATOR_LABELS: Record<DateOperator, string> = {
|
|
41
|
+
between: "Between",
|
|
42
|
+
after: "After",
|
|
43
|
+
before: "Before",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The operators offered for each {@link DateFilterMode}. */
|
|
47
|
+
const MODE_OPERATORS: Record<DateFilterMode, DateOperator[]> = {
|
|
48
|
+
range: ["between"],
|
|
49
|
+
comparison: ["after", "before"],
|
|
50
|
+
both: ["between", "after", "before"],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Pick the operator implied by an already-active filter (e.g. restored from the URL). */
|
|
54
|
+
function operatorFor(value: ActiveFilterValue | undefined): DateOperator {
|
|
55
|
+
const hasMin = !!value?.min;
|
|
56
|
+
const hasMax = !!value?.max;
|
|
57
|
+
if (hasMax && !hasMin) return "before";
|
|
58
|
+
if (hasMin && !hasMax) return "after";
|
|
59
|
+
return "between";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A date constraint whose chrome is driven by `mode`:
|
|
64
|
+
* - `"range"`: a single Between control (two date inputs).
|
|
65
|
+
* - `"comparison"` (default): an After / Before selector with one input.
|
|
66
|
+
* - `"both"`: a Between / After / Before selector spanning the two.
|
|
67
|
+
*
|
|
68
|
+
* "Between" shows two `<input type="date">` controls; "After" / "Before"
|
|
69
|
+
* collapse to one bound to the relevant end. Uses the platform-native picker —
|
|
70
|
+
* no popover/calendar dependency.
|
|
71
|
+
*/
|
|
72
|
+
export function DateRangeFilter({
|
|
73
|
+
field,
|
|
74
|
+
label,
|
|
75
|
+
helpText,
|
|
76
|
+
filterType = "daterange",
|
|
77
|
+
mode = "comparison",
|
|
78
|
+
}: DateRangeFilterProps) {
|
|
79
|
+
const { value, onChange } = useFilterField(field);
|
|
80
|
+
const operators = MODE_OPERATORS[mode];
|
|
81
|
+
|
|
82
|
+
// Operator is local UI state: it must survive a moment where no date is yet
|
|
83
|
+
// entered (which clears the active filter), so it can't be derived from
|
|
84
|
+
// `value` on every render. Seed it from any filter restored on mount, clamped
|
|
85
|
+
// to an operator this mode actually offers.
|
|
86
|
+
const [operator, setOperator] = useState<DateOperator>(() => {
|
|
87
|
+
const restored = operatorFor(value);
|
|
88
|
+
return operators.includes(restored) ? restored : operators[0];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const emit = (nextMin: string | undefined, nextMax: string | undefined) => {
|
|
92
|
+
if (!nextMin && !nextMax) onChange(undefined);
|
|
93
|
+
else onChange({ field, label, type: filterType, min: nextMin, max: nextMax });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const changeOperator = (next: DateOperator) => {
|
|
97
|
+
setOperator(next);
|
|
98
|
+
// Re-emit, keeping only the bound(s) the new operator uses.
|
|
99
|
+
if (next === "after") emit(value?.min, undefined);
|
|
100
|
+
else if (next === "before") emit(undefined, value?.max);
|
|
101
|
+
else emit(value?.min, value?.max);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const operatorId = `filter-${field}-op`;
|
|
105
|
+
// A lone "between" operator (mode="range") needs no selector — it can't change.
|
|
106
|
+
const showSelector = operators.length > 1;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<FilterFieldWrapper label={label} helpText={helpText}>
|
|
110
|
+
<div className="flex flex-col gap-2">
|
|
111
|
+
{showSelector && (
|
|
112
|
+
<Select value={operator} onValueChange={(v) => changeOperator(v as DateOperator)}>
|
|
113
|
+
<SelectTrigger id={operatorId} className="w-full" aria-label={`${label} comparison`}>
|
|
114
|
+
<SelectValue />
|
|
115
|
+
</SelectTrigger>
|
|
116
|
+
<SelectContent>
|
|
117
|
+
{operators.map((op) => (
|
|
118
|
+
<SelectItem key={op} value={op}>
|
|
119
|
+
{OPERATOR_LABELS[op]}
|
|
120
|
+
</SelectItem>
|
|
121
|
+
))}
|
|
122
|
+
</SelectContent>
|
|
123
|
+
</Select>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{operator === "between" ? (
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<Input
|
|
129
|
+
type="date"
|
|
130
|
+
value={value?.min ?? ""}
|
|
131
|
+
onChange={(e) => emit(e.target.value || undefined, value?.max)}
|
|
132
|
+
aria-label={`${label} from`}
|
|
133
|
+
/>
|
|
134
|
+
<span className="text-sm text-muted-foreground">–</span>
|
|
135
|
+
<Input
|
|
136
|
+
type="date"
|
|
137
|
+
value={value?.max ?? ""}
|
|
138
|
+
onChange={(e) => emit(value?.min, e.target.value || undefined)}
|
|
139
|
+
aria-label={`${label} to`}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
) : operator === "after" ? (
|
|
143
|
+
<Input
|
|
144
|
+
type="date"
|
|
145
|
+
value={value?.min ?? ""}
|
|
146
|
+
onChange={(e) => emit(e.target.value || undefined, undefined)}
|
|
147
|
+
aria-label={`${label} after`}
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<Input
|
|
151
|
+
type="date"
|
|
152
|
+
value={value?.max ?? ""}
|
|
153
|
+
onChange={(e) => emit(undefined, e.target.value || undefined)}
|
|
154
|
+
aria-label={`${label} before`}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</FilterFieldWrapper>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Label } from "../../../../../components/ui/label";
|
|
3
|
+
|
|
4
|
+
interface FilterFieldWrapperProps {
|
|
5
|
+
label: string;
|
|
6
|
+
htmlFor?: string;
|
|
7
|
+
helpText?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FilterFieldWrapper({
|
|
13
|
+
label,
|
|
14
|
+
htmlFor,
|
|
15
|
+
helpText,
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: FilterFieldWrapperProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`space-y-1 ${className ?? ""}`}>
|
|
21
|
+
<Label htmlFor={htmlFor}>{label}</Label>
|
|
22
|
+
{children}
|
|
23
|
+
{helpText && <p className="text-xs text-muted-foreground min-h-4">{helpText}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ChevronDown } from "lucide-react";
|
|
2
|
+
import { Button } from "../../../../../components/ui/button";
|
|
3
|
+
import { Checkbox } from "../../../../../components/ui/checkbox";
|
|
4
|
+
import { Label } from "../../../../../components/ui/label";
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
} from "../../../../../components/ui/popover";
|
|
10
|
+
import { useFilterField } from "../FilterContext";
|
|
11
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
12
|
+
|
|
13
|
+
interface MultiSelectFilterProps {
|
|
14
|
+
field: string;
|
|
15
|
+
label: string;
|
|
16
|
+
options: Array<{ value: string; label: string }>;
|
|
17
|
+
helpText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MultiSelectFilter({ field, label, options, helpText }: MultiSelectFilterProps) {
|
|
21
|
+
const { value, onChange } = useFilterField(field);
|
|
22
|
+
const selected = new Set(value?.value ? value.value.split(",").filter(Boolean) : []);
|
|
23
|
+
|
|
24
|
+
const toggle = (v: string, on: boolean) => {
|
|
25
|
+
const next = new Set(selected);
|
|
26
|
+
if (on) next.add(v);
|
|
27
|
+
else next.delete(v);
|
|
28
|
+
const joined = Array.from(next).join(",");
|
|
29
|
+
if (joined === "") onChange(undefined);
|
|
30
|
+
else onChange({ field, label, type: "multipicklist", value: joined });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Summarize the selection on the collapsed trigger: the sole label when one is
|
|
34
|
+
// picked, a count when several are, and a placeholder when none.
|
|
35
|
+
const summary =
|
|
36
|
+
selected.size === 0
|
|
37
|
+
? `Select ${label.toLowerCase()}`
|
|
38
|
+
: selected.size === 1
|
|
39
|
+
? (options.find((o) => selected.has(o.value))?.label ?? `${selected.size} selected`)
|
|
40
|
+
: `${selected.size} selected`;
|
|
41
|
+
const triggerId = `filter-${field}`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<FilterFieldWrapper label={label} htmlFor={triggerId} helpText={helpText}>
|
|
45
|
+
<Popover>
|
|
46
|
+
<PopoverTrigger asChild>
|
|
47
|
+
<Button
|
|
48
|
+
id={triggerId}
|
|
49
|
+
variant="outline"
|
|
50
|
+
className="w-full justify-between font-normal"
|
|
51
|
+
aria-label={`${label}: ${summary}`}
|
|
52
|
+
>
|
|
53
|
+
<span className={`truncate ${selected.size === 0 ? "text-muted-foreground" : ""}`}>
|
|
54
|
+
{summary}
|
|
55
|
+
</span>
|
|
56
|
+
<ChevronDown className="ml-2 size-4 shrink-0 opacity-50" aria-hidden="true" />
|
|
57
|
+
</Button>
|
|
58
|
+
</PopoverTrigger>
|
|
59
|
+
<PopoverContent
|
|
60
|
+
align="start"
|
|
61
|
+
className="w-(--radix-popover-trigger-width) max-h-72 overflow-y-auto"
|
|
62
|
+
>
|
|
63
|
+
<div role="group" aria-label={label} className="flex flex-col gap-1">
|
|
64
|
+
{options.length === 0 ? (
|
|
65
|
+
<p className="px-1 py-2 text-sm text-muted-foreground">No options available.</p>
|
|
66
|
+
) : (
|
|
67
|
+
options.map((opt) => {
|
|
68
|
+
const id = `filter-${field}-${opt.value}`;
|
|
69
|
+
return (
|
|
70
|
+
<div key={opt.value} className="flex items-center gap-2">
|
|
71
|
+
<Checkbox
|
|
72
|
+
id={id}
|
|
73
|
+
checked={selected.has(opt.value)}
|
|
74
|
+
onCheckedChange={(checked) => toggle(opt.value, checked === true)}
|
|
75
|
+
/>
|
|
76
|
+
<Label htmlFor={id} className="text-sm font-normal cursor-pointer">
|
|
77
|
+
{opt.label}
|
|
78
|
+
</Label>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
})
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</PopoverContent>
|
|
85
|
+
</Popover>
|
|
86
|
+
</FilterFieldWrapper>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Input } from "../../../../../components/ui/input";
|
|
3
|
+
import { useFilterField } from "../FilterContext";
|
|
4
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
5
|
+
import { debounce, FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
6
|
+
|
|
7
|
+
interface NumericRangeFilterProps {
|
|
8
|
+
field: string;
|
|
9
|
+
label: string;
|
|
10
|
+
helpText?: string;
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NumericRangeFilter({ field, label, helpText, min, max }: NumericRangeFilterProps) {
|
|
16
|
+
const { value, onChange } = useFilterField(field);
|
|
17
|
+
const [draftMin, setDraftMin] = useState(value?.min ?? "");
|
|
18
|
+
const [draftMax, setDraftMax] = useState(value?.max ?? "");
|
|
19
|
+
|
|
20
|
+
const onChangeRef = useRef(onChange);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
onChangeRef.current = onChange;
|
|
23
|
+
});
|
|
24
|
+
// Build the debounced caller in a mount effect, not during render: creating it
|
|
25
|
+
// inline would read onChangeRef.current at render time (react-hooks/refs). It
|
|
26
|
+
// stays a single stable instance and always calls the latest onChange via the ref.
|
|
27
|
+
const debouncedRef = useRef<((nextMin: string, nextMax: string) => void) | undefined>(undefined);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
debouncedRef.current = debounce((nextMin: string, nextMax: string) => {
|
|
30
|
+
if (!nextMin && !nextMax) onChangeRef.current(undefined);
|
|
31
|
+
else
|
|
32
|
+
onChangeRef.current({
|
|
33
|
+
field,
|
|
34
|
+
label,
|
|
35
|
+
type: "numeric",
|
|
36
|
+
min: nextMin || undefined,
|
|
37
|
+
max: nextMax || undefined,
|
|
38
|
+
});
|
|
39
|
+
}, FILTER_DEBOUNCE_MS);
|
|
40
|
+
}, [field, label]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setDraftMin(value?.min ?? "");
|
|
44
|
+
setDraftMax(value?.max ?? "");
|
|
45
|
+
}, [value?.min, value?.max]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<FilterFieldWrapper label={label} helpText={helpText}>
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<Input
|
|
51
|
+
type="number"
|
|
52
|
+
placeholder="Min"
|
|
53
|
+
min={min}
|
|
54
|
+
max={max}
|
|
55
|
+
value={draftMin}
|
|
56
|
+
onChange={(e) => {
|
|
57
|
+
setDraftMin(e.target.value);
|
|
58
|
+
debouncedRef.current?.(e.target.value, draftMax);
|
|
59
|
+
}}
|
|
60
|
+
aria-label={`${label} minimum`}
|
|
61
|
+
/>
|
|
62
|
+
<span className="text-sm text-muted-foreground">–</span>
|
|
63
|
+
<Input
|
|
64
|
+
type="number"
|
|
65
|
+
placeholder="Max"
|
|
66
|
+
min={min}
|
|
67
|
+
max={max}
|
|
68
|
+
value={draftMax}
|
|
69
|
+
onChange={(e) => {
|
|
70
|
+
setDraftMax(e.target.value);
|
|
71
|
+
debouncedRef.current?.(draftMin, e.target.value);
|
|
72
|
+
}}
|
|
73
|
+
aria-label={`${label} maximum`}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</FilterFieldWrapper>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Select,
|
|
3
|
+
SelectContent,
|
|
4
|
+
SelectItem,
|
|
5
|
+
SelectTrigger,
|
|
6
|
+
SelectValue,
|
|
7
|
+
} from "../../../../../components/ui/select";
|
|
8
|
+
import { useFilterField } from "../FilterContext";
|
|
9
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
10
|
+
|
|
11
|
+
const ALL_VALUE = "__all__";
|
|
12
|
+
|
|
13
|
+
interface SelectFilterProps {
|
|
14
|
+
field: string;
|
|
15
|
+
label: string;
|
|
16
|
+
options: Array<{ value: string; label: string }>;
|
|
17
|
+
helpText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SelectFilter({ field, label, options, helpText }: SelectFilterProps) {
|
|
21
|
+
const { value, onChange } = useFilterField(field);
|
|
22
|
+
const id = `filter-${field}`;
|
|
23
|
+
return (
|
|
24
|
+
<FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
|
|
25
|
+
<Select
|
|
26
|
+
value={value?.value ?? ALL_VALUE}
|
|
27
|
+
onValueChange={(v) => {
|
|
28
|
+
if (v === ALL_VALUE) onChange(undefined);
|
|
29
|
+
else onChange({ field, label, type: "picklist", value: v });
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<SelectTrigger id={id} className="w-full">
|
|
33
|
+
<SelectValue placeholder={`Select ${label.toLowerCase()}`} />
|
|
34
|
+
</SelectTrigger>
|
|
35
|
+
<SelectContent>
|
|
36
|
+
<SelectItem value={ALL_VALUE}>All</SelectItem>
|
|
37
|
+
{options.map((opt) => (
|
|
38
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
39
|
+
{opt.label}
|
|
40
|
+
</SelectItem>
|
|
41
|
+
))}
|
|
42
|
+
</SelectContent>
|
|
43
|
+
</Select>
|
|
44
|
+
</FilterFieldWrapper>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Input } from "../../../../../components/ui/input";
|
|
3
|
+
import { useFilterField } from "../FilterContext";
|
|
4
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
5
|
+
import { debounce, FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
6
|
+
|
|
7
|
+
interface TextFilterProps {
|
|
8
|
+
field: string;
|
|
9
|
+
label: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
helpText?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TextFilter({ field, label, placeholder, helpText }: TextFilterProps) {
|
|
15
|
+
const { value, onChange } = useFilterField(field);
|
|
16
|
+
const [draft, setDraft] = useState(value?.value ?? "");
|
|
17
|
+
|
|
18
|
+
const onChangeRef = useRef(onChange);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
onChangeRef.current = onChange;
|
|
21
|
+
});
|
|
22
|
+
// Build the debounced caller in a mount effect, not during render: creating it
|
|
23
|
+
// inline would read onChangeRef.current at render time (react-hooks/refs). It
|
|
24
|
+
// stays a single stable instance and always calls the latest onChange via the ref.
|
|
25
|
+
const debouncedRef = useRef<((next: string) => void) | undefined>(undefined);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
debouncedRef.current = debounce((next: string) => {
|
|
28
|
+
if (next === "") onChangeRef.current(undefined);
|
|
29
|
+
else onChangeRef.current({ field, label, type: "text", value: next });
|
|
30
|
+
}, FILTER_DEBOUNCE_MS);
|
|
31
|
+
}, [field, label]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setDraft(value?.value ?? "");
|
|
35
|
+
}, [value?.value]);
|
|
36
|
+
|
|
37
|
+
const id = `filter-${field}`;
|
|
38
|
+
return (
|
|
39
|
+
<FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
|
|
40
|
+
<Input
|
|
41
|
+
id={id}
|
|
42
|
+
type="text"
|
|
43
|
+
value={draft}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
setDraft(e.target.value);
|
|
47
|
+
debouncedRef.current?.(e.target.value);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</FilterFieldWrapper>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default per-node row used when no `renderResult` is supplied for a source.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - Title: first entry in `displayFields` (preferring displayValue).
|
|
6
|
+
* - Subtitle: remaining display fields joined with " · ".
|
|
7
|
+
* - When `routePattern` is configured on the source, the row is wrapped in
|
|
8
|
+
* a react-router `<Link>` whose `to` is built by substituting `:fieldName`
|
|
9
|
+
* tokens with the corresponding field values (special token `:id`
|
|
10
|
+
* resolves to the configured `idField`, default "Id").
|
|
11
|
+
*
|
|
12
|
+
* Applications can still pass `renderResult[sourceKey]` to override.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { Link } from "react-router";
|
|
17
|
+
import { fieldValue } from "../../utils/fieldUtils";
|
|
18
|
+
import type { DisplayField, SObjectSourceConfig } from "../../types";
|
|
19
|
+
|
|
20
|
+
const ROUTE_TOKEN = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
21
|
+
|
|
22
|
+
function fieldName(f: DisplayField): string {
|
|
23
|
+
return typeof f === "string" ? f : f.name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolves a single dot-path against a node, returning a string or null. */
|
|
27
|
+
function readPath(node: unknown, path: string): string | null {
|
|
28
|
+
if (!node || typeof node !== "object") return null;
|
|
29
|
+
const parts = path.split(".");
|
|
30
|
+
let cursor: unknown = node;
|
|
31
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
32
|
+
if (!cursor || typeof cursor !== "object") return null;
|
|
33
|
+
cursor = (cursor as Record<string, unknown>)[parts[i]];
|
|
34
|
+
}
|
|
35
|
+
const leaf = (cursor as Record<string, unknown> | null)?.[parts[parts.length - 1]];
|
|
36
|
+
if (leaf == null) return null;
|
|
37
|
+
if (typeof leaf === "object") {
|
|
38
|
+
return fieldValue(leaf as { value?: unknown; displayValue?: string | null });
|
|
39
|
+
}
|
|
40
|
+
return String(leaf);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reads the user-facing string for a top-level display field. */
|
|
44
|
+
function readDisplayField(node: unknown, field: DisplayField): string | null {
|
|
45
|
+
if (typeof field === "string") return readPath(node, field);
|
|
46
|
+
if ("raw" in field) return readPath(node, field.name);
|
|
47
|
+
// Relationship: prefer the first scalar subfield's display value.
|
|
48
|
+
const sub = field.subfields[0];
|
|
49
|
+
if (!sub) return null;
|
|
50
|
+
const subName = fieldName(sub);
|
|
51
|
+
return readPath(node, `${field.name}.${subName}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Substitutes `:fieldName` tokens in routePattern with node values. */
|
|
55
|
+
function resolveRoute(pattern: string, node: unknown, idField: string): string | null {
|
|
56
|
+
let failed = false;
|
|
57
|
+
const out = pattern.replace(ROUTE_TOKEN, (_, token) => {
|
|
58
|
+
const path = token === "id" ? idField : token;
|
|
59
|
+
const value = readPath(node, path);
|
|
60
|
+
if (value == null) {
|
|
61
|
+
failed = true;
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
return encodeURIComponent(value);
|
|
65
|
+
});
|
|
66
|
+
return failed ? null : out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DefaultResultRowProps {
|
|
70
|
+
node: unknown;
|
|
71
|
+
source: SObjectSourceConfig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function DefaultResultRow({ node, source }: DefaultResultRowProps) {
|
|
75
|
+
const idField = source.idField ?? "Id";
|
|
76
|
+
const fields = source.displayFields;
|
|
77
|
+
const titleField = fields[0];
|
|
78
|
+
const title = titleField ? readDisplayField(node, titleField) : null;
|
|
79
|
+
|
|
80
|
+
const subtitleParts = fields
|
|
81
|
+
.slice(1)
|
|
82
|
+
.map((f) => readDisplayField(node, f))
|
|
83
|
+
.filter((s): s is string => s != null && s !== "");
|
|
84
|
+
|
|
85
|
+
const content: ReactNode = (
|
|
86
|
+
<>
|
|
87
|
+
<span className="font-medium">{title ?? "—"}</span>
|
|
88
|
+
{subtitleParts.length > 0 && (
|
|
89
|
+
<p className="text-sm text-muted-foreground">{subtitleParts.join(" · ")}</p>
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (source.routePattern) {
|
|
95
|
+
const href = resolveRoute(source.routePattern, node, idField);
|
|
96
|
+
if (href) {
|
|
97
|
+
return (
|
|
98
|
+
<Link
|
|
99
|
+
to={href}
|
|
100
|
+
className="block hover:bg-accent rounded-md px-3 -mx-3 py-1 transition-colors"
|
|
101
|
+
>
|
|
102
|
+
{content}
|
|
103
|
+
</Link>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return <div className="px-3 -mx-3 py-1">{content}</div>;
|
|
109
|
+
}
|
package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/config.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pagination": {
|
|
3
|
+
"pageSize": 10,
|
|
4
|
+
"pageSizeOptions": [5, 10, 20]
|
|
5
|
+
},
|
|
6
|
+
"sources": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "sobject",
|
|
9
|
+
"key": "accounts",
|
|
10
|
+
"objectName": "Account",
|
|
11
|
+
"label": "Accounts",
|
|
12
|
+
"labelSingular": "Account",
|
|
13
|
+
"routePattern": "/accounts/:id",
|
|
14
|
+
"searchableFields": ["Name", "Phone", "Industry"],
|
|
15
|
+
"displayFields": [
|
|
16
|
+
"Name",
|
|
17
|
+
"Industry",
|
|
18
|
+
"Type",
|
|
19
|
+
"Phone",
|
|
20
|
+
"AnnualRevenue",
|
|
21
|
+
{ "name": "Owner", "subfields": ["Name"] }
|
|
22
|
+
],
|
|
23
|
+
"filterBy": [
|
|
24
|
+
{ "field": "Industry", "label": "Industry", "type": "picklist" },
|
|
25
|
+
{ "field": "Type", "label": "Type", "type": "picklist" },
|
|
26
|
+
{ "field": "AnnualRevenue", "label": "Annual Revenue", "type": "numeric" }
|
|
27
|
+
],
|
|
28
|
+
"sortBy": [
|
|
29
|
+
{ "field": "Name", "label": "Name" },
|
|
30
|
+
{ "field": "Industry", "label": "Industry" },
|
|
31
|
+
{ "field": "AnnualRevenue", "label": "Annual Revenue" },
|
|
32
|
+
{ "field": "CreatedDate", "label": "Created Date" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"kind": "sobject",
|
|
37
|
+
"key": "contacts",
|
|
38
|
+
"objectName": "Contact",
|
|
39
|
+
"label": "Contacts",
|
|
40
|
+
"labelSingular": "Contact",
|
|
41
|
+
"routePattern": "/contacts/:id",
|
|
42
|
+
"searchableFields": ["Name", "Email", "Phone"],
|
|
43
|
+
"displayFields": [
|
|
44
|
+
"Name",
|
|
45
|
+
"Title",
|
|
46
|
+
"Email",
|
|
47
|
+
"Phone",
|
|
48
|
+
{ "name": "Account", "subfields": ["Name"] }
|
|
49
|
+
],
|
|
50
|
+
"sortBy": [
|
|
51
|
+
{ "field": "Name", "label": "Name" },
|
|
52
|
+
{ "field": "CreatedDate", "label": "Created Date" }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"kind": "sobject",
|
|
57
|
+
"key": "opportunities",
|
|
58
|
+
"objectName": "Opportunity",
|
|
59
|
+
"label": "Opportunities",
|
|
60
|
+
"labelSingular": "Opportunity",
|
|
61
|
+
"routePattern": "/opportunities/:id",
|
|
62
|
+
"searchableFields": ["Name"],
|
|
63
|
+
"displayFields": [
|
|
64
|
+
"Name",
|
|
65
|
+
"StageName",
|
|
66
|
+
"Amount",
|
|
67
|
+
"CloseDate",
|
|
68
|
+
{ "name": "Account", "subfields": ["Name"] }
|
|
69
|
+
],
|
|
70
|
+
"filterBy": [
|
|
71
|
+
{ "field": "StageName", "label": "Stage", "type": "picklist" },
|
|
72
|
+
{ "field": "Amount", "label": "Amount", "type": "numeric" },
|
|
73
|
+
{ "field": "CloseDate", "label": "Close Date", "type": "daterange" }
|
|
74
|
+
],
|
|
75
|
+
"sortBy": [
|
|
76
|
+
{ "field": "CloseDate", "label": "Close Date" },
|
|
77
|
+
{ "field": "Amount", "label": "Amount" },
|
|
78
|
+
{ "field": "Name", "label": "Name" }
|
|
79
|
+
],
|
|
80
|
+
"defaultSort": { "field": "CloseDate", "direction": "DESC" }
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
@@ -7,13 +7,9 @@ interface UseAsyncDataResult<T> {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Runs an async fetcher on mount and whenever `deps` change.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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).
|
|
10
|
+
* Runs an async fetcher on mount and whenever `deps` change. Stale responses
|
|
11
|
+
* (deps changed during a fetch, or component unmounted) are dropped via a
|
|
12
|
+
* cancellation flag.
|
|
17
13
|
*/
|
|
18
14
|
export function useAsyncData<T>(
|
|
19
15
|
fetcher: () => Promise<T>,
|
|
@@ -29,7 +25,6 @@ export function useAsyncData<T>(
|
|
|
29
25
|
fetcherRef.current = fetcher;
|
|
30
26
|
});
|
|
31
27
|
|
|
32
|
-
// Detect dep changes during render to reset loading state and bump generation
|
|
33
28
|
const [prevDeps, setPrevDeps] = useState(deps);
|
|
34
29
|
if (deps.length !== prevDeps.length || deps.some((d, i) => d !== prevDeps[i])) {
|
|
35
30
|
setPrevDeps(deps);
|
|
@@ -40,7 +35,6 @@ export function useAsyncData<T>(
|
|
|
40
35
|
|
|
41
36
|
useEffect(() => {
|
|
42
37
|
let cancelled = false;
|
|
43
|
-
|
|
44
38
|
fetcherRef
|
|
45
39
|
.current()
|
|
46
40
|
.then((result) => {
|
|
@@ -53,7 +47,6 @@ export function useAsyncData<T>(
|
|
|
53
47
|
.finally(() => {
|
|
54
48
|
if (!cancelled) setLoading(false);
|
|
55
49
|
});
|
|
56
|
-
|
|
57
50
|
return () => {
|
|
58
51
|
cancelled = true;
|
|
59
52
|
};
|