@salesforce/webapp-template-feature-react-global-search-experimental 1.3.3
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/LICENSE.txt +82 -0
- package/README.md +415 -0
- package/dist/.a4drules/build-validation.md +81 -0
- package/dist/.a4drules/code-quality.md +150 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +211 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
- package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
- package/dist/.a4drules/graphql.md +98 -0
- package/dist/.a4drules/images.md +13 -0
- package/dist/.a4drules/react.md +361 -0
- package/dist/.a4drules/react_image_processing.md +45 -0
- package/dist/.a4drules/typescript.md +224 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +11 -0
- package/dist/README.md +18 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierignore +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierrc +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/eslint.config.js +113 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/feature-react-global-search.webapplication-meta.xml +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/index.html +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/package.json +42 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/graphql-operations-types.ts +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/objectInfoService.ts +229 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/app.tsx +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/appLayout.tsx +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/FiltersPanel.tsx +373 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/SearchResultCard.tsx +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailFields.tsx +57 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailHeader.tsx +42 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchHeader.tsx +23 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchPagination.tsx +162 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchResultsPanel.tsx +184 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/shared/GlobalSearchInput.tsx +110 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/alert.tsx +65 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/button.tsx +56 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/card.tsx +77 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/field.tsx +111 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/index.ts +71 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/label.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/pagination.tsx +99 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/select.tsx +151 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/skeleton.tsx +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/spinner.tsx +21 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/tabs.tsx +115 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/constants.ts +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/index.ts +65 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/form.tsx +208 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useObjectSearchData.ts +419 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useRecordDetail.ts +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/About.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/DetailPage.tsx +128 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/GlobalSearch.tsx +173 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/Home.tsx +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/routes.tsx +50 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/styles/global.css +108 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/filters.ts +122 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/search/searchResults.ts +228 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/formUtils.ts +130 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/recordUtils.ts +75 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.json +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vite.config.ts +82 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.config.ts +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.setup.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/webapplication.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package.json +37 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +32 -0
package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/form.tsx
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
|
|
3
|
+
import { Field, FieldDescription, FieldError, FieldLabel } from "../components/ui/field";
|
|
4
|
+
import { Input } from "../components/ui/input";
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "../components/ui/select";
|
|
12
|
+
import { cn } from "../lib/utils";
|
|
13
|
+
import type { PicklistValue } from "../types/filters/picklist";
|
|
14
|
+
import { getUniqueErrors } from "../utils/formUtils";
|
|
15
|
+
|
|
16
|
+
export type { FormError } from "../utils/formUtils";
|
|
17
|
+
export { validateRangeValues } from "../utils/formUtils";
|
|
18
|
+
|
|
19
|
+
export const { fieldContext, formContext, useFieldContext, useFormContext } =
|
|
20
|
+
createFormHookContexts();
|
|
21
|
+
|
|
22
|
+
interface FilterTextFieldProps
|
|
23
|
+
extends Omit<
|
|
24
|
+
React.ComponentProps<typeof Input>,
|
|
25
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
26
|
+
> {
|
|
27
|
+
label: string;
|
|
28
|
+
description?: React.ReactNode;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function FilterTextField({
|
|
33
|
+
label,
|
|
34
|
+
id: providedId,
|
|
35
|
+
description,
|
|
36
|
+
placeholder,
|
|
37
|
+
type = "text",
|
|
38
|
+
...props
|
|
39
|
+
}: FilterTextFieldProps) {
|
|
40
|
+
const field = useFieldContext<string>();
|
|
41
|
+
const generatedId = useId();
|
|
42
|
+
const id = providedId ?? generatedId;
|
|
43
|
+
const descriptionId = `${id}-description`;
|
|
44
|
+
const errorId = `${id}-error`;
|
|
45
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
46
|
+
|
|
47
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Field data-invalid={isInvalid}>
|
|
51
|
+
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
52
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
53
|
+
<Input
|
|
54
|
+
id={id}
|
|
55
|
+
name={field.name as string}
|
|
56
|
+
type={type}
|
|
57
|
+
value={field.state.value ?? ""}
|
|
58
|
+
onBlur={field.handleBlur}
|
|
59
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => field.handleChange(e.target.value)}
|
|
60
|
+
placeholder={placeholder}
|
|
61
|
+
aria-invalid={isInvalid}
|
|
62
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
66
|
+
</Field>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface FilterSelectFieldProps {
|
|
71
|
+
label: string;
|
|
72
|
+
id?: string;
|
|
73
|
+
description?: React.ReactNode;
|
|
74
|
+
placeholder?: string;
|
|
75
|
+
options: PicklistValue[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function FilterSelectField({
|
|
79
|
+
label,
|
|
80
|
+
id: providedId,
|
|
81
|
+
description,
|
|
82
|
+
placeholder = "Select...",
|
|
83
|
+
options,
|
|
84
|
+
}: FilterSelectFieldProps) {
|
|
85
|
+
const field = useFieldContext<string>();
|
|
86
|
+
const generatedId = useId();
|
|
87
|
+
const id = providedId ?? generatedId;
|
|
88
|
+
const descriptionId = `${id}-description`;
|
|
89
|
+
const errorId = `${id}-error`;
|
|
90
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
91
|
+
|
|
92
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Field data-invalid={isInvalid}>
|
|
96
|
+
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
97
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
98
|
+
<Select value={field.state.value ?? ""} onValueChange={(value) => field.handleChange(value)}>
|
|
99
|
+
<SelectTrigger
|
|
100
|
+
id={id}
|
|
101
|
+
aria-invalid={isInvalid}
|
|
102
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
103
|
+
>
|
|
104
|
+
<SelectValue placeholder={placeholder} />
|
|
105
|
+
</SelectTrigger>
|
|
106
|
+
<SelectContent>
|
|
107
|
+
{options.map((option) => {
|
|
108
|
+
if (!option || !option.value) return null;
|
|
109
|
+
return (
|
|
110
|
+
<SelectItem key={option.value} value={option.value}>
|
|
111
|
+
{option.label || option.value}
|
|
112
|
+
</SelectItem>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</SelectContent>
|
|
116
|
+
</Select>
|
|
117
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
118
|
+
</Field>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface FilterRangeFieldProps
|
|
123
|
+
extends Omit<
|
|
124
|
+
React.ComponentProps<typeof Input>,
|
|
125
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
126
|
+
> {
|
|
127
|
+
label?: string;
|
|
128
|
+
description?: React.ReactNode;
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function FilterRangeFieldBase({
|
|
133
|
+
label,
|
|
134
|
+
id: providedId,
|
|
135
|
+
description,
|
|
136
|
+
placeholder,
|
|
137
|
+
type = "text",
|
|
138
|
+
...props
|
|
139
|
+
}: FilterRangeFieldProps) {
|
|
140
|
+
const field = useFieldContext<string>();
|
|
141
|
+
const generatedId = useId();
|
|
142
|
+
const id = providedId ?? generatedId;
|
|
143
|
+
const descriptionId = `${id}-description`;
|
|
144
|
+
const errorId = `${id}-error`;
|
|
145
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
146
|
+
|
|
147
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
{label && <FieldLabel htmlFor={id}>{label}</FieldLabel>}
|
|
152
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
153
|
+
<Input
|
|
154
|
+
id={id}
|
|
155
|
+
name={field.name as string}
|
|
156
|
+
type={type}
|
|
157
|
+
value={field.state.value ?? ""}
|
|
158
|
+
onBlur={field.handleBlur}
|
|
159
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => field.handleChange(e.target.value)}
|
|
160
|
+
placeholder={placeholder}
|
|
161
|
+
aria-invalid={isInvalid}
|
|
162
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface FilterRangeMinFieldProps
|
|
171
|
+
extends Omit<
|
|
172
|
+
React.ComponentProps<typeof Input>,
|
|
173
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
174
|
+
> {
|
|
175
|
+
label?: string;
|
|
176
|
+
description?: React.ReactNode;
|
|
177
|
+
placeholder?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface FilterRangeMaxFieldProps
|
|
181
|
+
extends Omit<
|
|
182
|
+
React.ComponentProps<typeof Input>,
|
|
183
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
184
|
+
> {
|
|
185
|
+
label?: string;
|
|
186
|
+
description?: React.ReactNode;
|
|
187
|
+
placeholder?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function FilterRangeMinField({ placeholder = "Min", ...props }: FilterRangeMinFieldProps) {
|
|
191
|
+
return <FilterRangeFieldBase placeholder={placeholder} {...props} />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function FilterRangeMaxField({ placeholder = "Max", ...props }: FilterRangeMaxFieldProps) {
|
|
195
|
+
return <FilterRangeFieldBase placeholder={placeholder} {...props} />;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const { useAppForm } = createFormHook({
|
|
199
|
+
fieldContext,
|
|
200
|
+
formContext,
|
|
201
|
+
fieldComponents: {
|
|
202
|
+
FilterTextField,
|
|
203
|
+
FilterSelectField,
|
|
204
|
+
FilterRangeMinField,
|
|
205
|
+
FilterRangeMaxField,
|
|
206
|
+
},
|
|
207
|
+
formComponents: {},
|
|
208
|
+
});
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Search Data Hooks
|
|
3
|
+
*
|
|
4
|
+
* Custom hooks for managing object search data including columns, results, and filters.
|
|
5
|
+
* Each hook is responsible for a specific aspect of the search functionality.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* All hooks coordinate API calls separately from UI components.
|
|
9
|
+
* This ensures proper separation of concerns and allows for coordinated chained calls per page.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
13
|
+
import { objectInfoService, type SearchParams } from "../api/objectInfoService";
|
|
14
|
+
import type {
|
|
15
|
+
Column,
|
|
16
|
+
SearchResultRecord,
|
|
17
|
+
SearchResultRecordData,
|
|
18
|
+
} from "../types/search/searchResults";
|
|
19
|
+
import type { Filter, FilterCriteria } from "../types/filters/filters";
|
|
20
|
+
import type { PicklistValue } from "../types/filters/picklist";
|
|
21
|
+
import { createFiltersKey } from "../utils/cacheUtils";
|
|
22
|
+
|
|
23
|
+
// --- Shared Types ---
|
|
24
|
+
export interface FiltersData {
|
|
25
|
+
filters: Filter[];
|
|
26
|
+
picklistValues: Record<string, PicklistValue[]>;
|
|
27
|
+
loading: boolean;
|
|
28
|
+
error: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook: useObjectColumns
|
|
33
|
+
*
|
|
34
|
+
* Fetches and caches column definitions for a specific object.
|
|
35
|
+
* Columns are metadata that rarely changes, so they are cached per object.
|
|
36
|
+
*
|
|
37
|
+
* @param objectApiName - The API name of the object to fetch columns for
|
|
38
|
+
* @returns Object containing columns array, loading state, and error state
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const { columns, columnsLoading, columnsError } = useObjectColumns('Account');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useObjectColumns(objectApiName: string | null) {
|
|
46
|
+
const [columnsCache, setColumnsCache] = useState<Record<string, Column[]>>({});
|
|
47
|
+
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
|
48
|
+
const [error, setError] = useState<Record<string, string | null>>({});
|
|
49
|
+
|
|
50
|
+
const columnsCacheRef = useRef(columnsCache);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
columnsCacheRef.current = columnsCache;
|
|
53
|
+
}, [columnsCache]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!objectApiName) return;
|
|
57
|
+
|
|
58
|
+
if (columnsCacheRef.current[objectApiName]) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let isCancelled = false;
|
|
63
|
+
const abortController = new AbortController();
|
|
64
|
+
|
|
65
|
+
const fetchColumns = async () => {
|
|
66
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: true }));
|
|
67
|
+
setError((prev) => ({ ...prev, [objectApiName]: null }));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const columns = await objectInfoService.getObjectListInfo(
|
|
71
|
+
objectApiName,
|
|
72
|
+
"__SearchResult",
|
|
73
|
+
abortController.signal,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (isCancelled) return;
|
|
77
|
+
|
|
78
|
+
setColumnsCache((prev) => ({ ...prev, [objectApiName]: columns }));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setError((prev) => ({ ...prev, [objectApiName]: "Failed to load columns" }));
|
|
84
|
+
} finally {
|
|
85
|
+
if (!isCancelled) {
|
|
86
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: false }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
fetchColumns();
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
isCancelled = true;
|
|
95
|
+
abortController.abort();
|
|
96
|
+
};
|
|
97
|
+
}, [objectApiName]);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
columns: objectApiName ? columnsCache[objectApiName] || [] : [],
|
|
101
|
+
columnsLoading: objectApiName ? loading[objectApiName] || false : false,
|
|
102
|
+
columnsError: objectApiName ? error[objectApiName] || null : null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Hook: useObjectSearchResults
|
|
108
|
+
*
|
|
109
|
+
* Fetches search results for a specific object based on the provided query parameters.
|
|
110
|
+
* Maintains the *latest* result set for the object in state to prevent redundant
|
|
111
|
+
* network requests when the component re-renders with the same parameters.
|
|
112
|
+
* Includes debouncing for search queries (but not pagination).
|
|
113
|
+
*
|
|
114
|
+
* @param objectApiName - The API name of the object to search
|
|
115
|
+
* @param searchQuery - The search query string
|
|
116
|
+
* @param searchPageSize - Number of results per page (default: 50)
|
|
117
|
+
* @param searchPageToken - Pagination token (default: '0')
|
|
118
|
+
* @param filters - Array of filter criteria to apply (default: [])
|
|
119
|
+
* @param sortBy - Sort field and direction (default: 'relevance')
|
|
120
|
+
* @returns Object containing results array, pagination tokens, loading state, and error state
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* const { results, nextPageToken, previousPageToken, currentPageToken, resultsLoading, resultsError } = useObjectSearchResults(
|
|
125
|
+
* 'Account',
|
|
126
|
+
* 'test query',
|
|
127
|
+
* 25,
|
|
128
|
+
* '0',
|
|
129
|
+
* [{ objectApiName: 'Account', fieldPath: 'Name', operator: 'contains', values: ['test'] }]
|
|
130
|
+
* );
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function useObjectSearchResults(
|
|
134
|
+
objectApiName: string | null,
|
|
135
|
+
searchQuery: string,
|
|
136
|
+
searchPageSize: number = 50,
|
|
137
|
+
searchPageToken: string = "0",
|
|
138
|
+
filters: FilterCriteria[] = [],
|
|
139
|
+
sortBy: string = "relevance",
|
|
140
|
+
) {
|
|
141
|
+
const [resultsCache, setResultsCache] = useState<
|
|
142
|
+
Record<
|
|
143
|
+
string,
|
|
144
|
+
{
|
|
145
|
+
results: SearchResultRecord[];
|
|
146
|
+
query: string;
|
|
147
|
+
pageToken: string;
|
|
148
|
+
pageSize: number;
|
|
149
|
+
filtersKey: string;
|
|
150
|
+
sortBy: string;
|
|
151
|
+
nextPageToken: string | null;
|
|
152
|
+
previousPageToken: string | null;
|
|
153
|
+
currentPageToken: string;
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
>({});
|
|
157
|
+
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
|
158
|
+
const [error, setError] = useState<Record<string, string | null>>({});
|
|
159
|
+
|
|
160
|
+
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
161
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
162
|
+
const resultsCacheRef = useRef(resultsCache);
|
|
163
|
+
|
|
164
|
+
const filtersKey = useMemo(() => {
|
|
165
|
+
const filtersArray = Array.isArray(filters) ? filters : [];
|
|
166
|
+
return createFiltersKey(filtersArray);
|
|
167
|
+
}, [filters]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
resultsCacheRef.current = resultsCache;
|
|
171
|
+
}, [resultsCache]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!objectApiName || !searchQuery.trim()) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let isCancelled = false;
|
|
179
|
+
const abortController = new AbortController();
|
|
180
|
+
|
|
181
|
+
if (abortControllerRef.current) {
|
|
182
|
+
abortControllerRef.current.abort();
|
|
183
|
+
}
|
|
184
|
+
abortControllerRef.current = abortController;
|
|
185
|
+
|
|
186
|
+
if (debounceTimeout.current) {
|
|
187
|
+
clearTimeout(debounceTimeout.current);
|
|
188
|
+
debounceTimeout.current = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const cached = resultsCacheRef.current[objectApiName];
|
|
192
|
+
if (
|
|
193
|
+
!abortController.signal.aborted &&
|
|
194
|
+
cached &&
|
|
195
|
+
cached.query === searchQuery &&
|
|
196
|
+
cached.pageToken === searchPageToken &&
|
|
197
|
+
cached.pageSize === searchPageSize &&
|
|
198
|
+
cached.filtersKey === filtersKey &&
|
|
199
|
+
cached.sortBy === sortBy
|
|
200
|
+
) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (abortController.signal.aborted) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const fetchResults = async () => {
|
|
209
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: true }));
|
|
210
|
+
setError((prev) => ({ ...prev, [objectApiName]: null }));
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const searchParams: SearchParams = {
|
|
214
|
+
sortBy: sortBy === "relevance" ? "" : sortBy,
|
|
215
|
+
filters: filters,
|
|
216
|
+
pageSize: searchPageSize,
|
|
217
|
+
pageToken: searchPageToken,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const keywordSearchResult = await objectInfoService.searchResults(
|
|
221
|
+
searchQuery,
|
|
222
|
+
objectApiName,
|
|
223
|
+
searchParams,
|
|
224
|
+
abortController.signal,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (isCancelled || abortController.signal.aborted) return;
|
|
228
|
+
|
|
229
|
+
const normalizedRecords = keywordSearchResult.records.map((r) => ({
|
|
230
|
+
record: r.record as SearchResultRecordData,
|
|
231
|
+
highlightInfo: r.highlightInfo,
|
|
232
|
+
searchInfo: r.searchInfo,
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
const nextPageToken: string | null = keywordSearchResult.nextPageToken ?? null;
|
|
236
|
+
const previousPageToken: string | null = keywordSearchResult.previousPageToken ?? null;
|
|
237
|
+
|
|
238
|
+
setResultsCache((prev): typeof prev => ({
|
|
239
|
+
...prev,
|
|
240
|
+
[objectApiName]: {
|
|
241
|
+
results: normalizedRecords,
|
|
242
|
+
query: searchQuery,
|
|
243
|
+
pageToken: searchPageToken,
|
|
244
|
+
pageSize: searchPageSize,
|
|
245
|
+
filtersKey: filtersKey,
|
|
246
|
+
sortBy,
|
|
247
|
+
nextPageToken,
|
|
248
|
+
previousPageToken,
|
|
249
|
+
currentPageToken: keywordSearchResult.currentPageToken,
|
|
250
|
+
},
|
|
251
|
+
}));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
setError((prev) => ({ ...prev, [objectApiName]: "Unable to load search results" }));
|
|
257
|
+
} finally {
|
|
258
|
+
if (!isCancelled) {
|
|
259
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: false }));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (searchPageToken === "0") {
|
|
265
|
+
debounceTimeout.current = setTimeout(() => {
|
|
266
|
+
fetchResults();
|
|
267
|
+
}, 300);
|
|
268
|
+
} else {
|
|
269
|
+
fetchResults();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return () => {
|
|
273
|
+
isCancelled = true;
|
|
274
|
+
abortController.abort();
|
|
275
|
+
if (debounceTimeout.current) {
|
|
276
|
+
clearTimeout(debounceTimeout.current);
|
|
277
|
+
debounceTimeout.current = null;
|
|
278
|
+
}
|
|
279
|
+
if (abortControllerRef.current === abortController) {
|
|
280
|
+
abortControllerRef.current = null;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}, [objectApiName, searchQuery, searchPageSize, searchPageToken, filtersKey, sortBy]);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
results: objectApiName ? resultsCache[objectApiName]?.results || [] : [],
|
|
287
|
+
nextPageToken: objectApiName ? resultsCache[objectApiName]?.nextPageToken || null : null,
|
|
288
|
+
previousPageToken: objectApiName
|
|
289
|
+
? resultsCache[objectApiName]?.previousPageToken || null
|
|
290
|
+
: null,
|
|
291
|
+
currentPageToken: objectApiName ? resultsCache[objectApiName]?.currentPageToken || "0" : "0",
|
|
292
|
+
resultsLoading: objectApiName ? loading[objectApiName] || false : false,
|
|
293
|
+
resultsError: objectApiName ? error[objectApiName] || null : null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Hook: useObjectFilters
|
|
299
|
+
*
|
|
300
|
+
* Fetches and caches filter definitions and picklist values for a specific object.
|
|
301
|
+
* Filters and picklists are fetched in parallel to avoid network waterfalls.
|
|
302
|
+
*
|
|
303
|
+
* @param objectApiName - The API name of the object to fetch filters for
|
|
304
|
+
* @returns Object containing filtersData record keyed by object API name
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```tsx
|
|
308
|
+
* const { filtersData } = useObjectFilters('Account');
|
|
309
|
+
* const filtersInfo = filtersData['Account'];
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function useObjectFilters(objectApiName: string | null) {
|
|
313
|
+
const [filtersCache, setFiltersCache] = useState<Record<string, FiltersData>>({});
|
|
314
|
+
|
|
315
|
+
const filtersCacheRef = useRef(filtersCache);
|
|
316
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
filtersCacheRef.current = filtersCache;
|
|
320
|
+
}, [filtersCache]);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!objectApiName) return;
|
|
324
|
+
|
|
325
|
+
const cached = filtersCacheRef.current[objectApiName];
|
|
326
|
+
if (cached && (cached.filters?.length > 0 || cached.loading)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (abortControllerRef.current) {
|
|
331
|
+
abortControllerRef.current.abort();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let isCancelled = false;
|
|
335
|
+
const abortController = new AbortController();
|
|
336
|
+
abortControllerRef.current = abortController;
|
|
337
|
+
|
|
338
|
+
const fetchFilters = async () => {
|
|
339
|
+
setFiltersCache((prev) => ({
|
|
340
|
+
...prev,
|
|
341
|
+
[objectApiName]: {
|
|
342
|
+
filters: prev[objectApiName]?.filters || [],
|
|
343
|
+
picklistValues: prev[objectApiName]?.picklistValues || {},
|
|
344
|
+
loading: true,
|
|
345
|
+
error: null,
|
|
346
|
+
},
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const filters = await objectInfoService.getObjectListFilters(
|
|
351
|
+
objectApiName,
|
|
352
|
+
abortController.signal,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (isCancelled) return;
|
|
356
|
+
|
|
357
|
+
const selectFilters = filters.filter((f) => f.affordance?.toLowerCase() === "select");
|
|
358
|
+
|
|
359
|
+
const picklistPromises = selectFilters.map((f) =>
|
|
360
|
+
objectInfoService
|
|
361
|
+
.getPicklistValues(objectApiName, f.targetFieldPath, undefined, abortController.signal)
|
|
362
|
+
.then((values) => ({ fieldPath: f.targetFieldPath, values }))
|
|
363
|
+
.catch((err) => {
|
|
364
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
return { fieldPath: f.targetFieldPath, values: [] };
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const picklistResults = await Promise.all(picklistPromises);
|
|
372
|
+
|
|
373
|
+
if (isCancelled) return;
|
|
374
|
+
|
|
375
|
+
const picklistValuesRecord: Record<string, PicklistValue[]> = {};
|
|
376
|
+
picklistResults.forEach(({ fieldPath, values }) => {
|
|
377
|
+
picklistValuesRecord[fieldPath] = values;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
setFiltersCache((prev) => ({
|
|
381
|
+
...prev,
|
|
382
|
+
[objectApiName]: {
|
|
383
|
+
filters,
|
|
384
|
+
picklistValues: picklistValuesRecord,
|
|
385
|
+
loading: false,
|
|
386
|
+
error: null,
|
|
387
|
+
},
|
|
388
|
+
}));
|
|
389
|
+
} catch (err) {
|
|
390
|
+
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
setFiltersCache((prev) => ({
|
|
394
|
+
...prev,
|
|
395
|
+
[objectApiName]: {
|
|
396
|
+
filters: [],
|
|
397
|
+
picklistValues: {},
|
|
398
|
+
loading: false,
|
|
399
|
+
error: "Failed to load filters",
|
|
400
|
+
},
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
fetchFilters();
|
|
406
|
+
|
|
407
|
+
return () => {
|
|
408
|
+
isCancelled = true;
|
|
409
|
+
abortController.abort();
|
|
410
|
+
if (abortControllerRef.current === abortController) {
|
|
411
|
+
abortControllerRef.current = null;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}, [objectApiName]);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
filtersData: filtersCache,
|
|
418
|
+
};
|
|
419
|
+
}
|