@salesforce/ui-bundle-template-app-react-template-b2x 11.17.1 → 11.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/components → components}/ObjectBreadcrumb.tsx +7 -2
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/distinctValuesService.ts +84 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/searchService.ts +71 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/MergedSearchResults.tsx +263 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/Search.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SearchResults.tsx +77 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SourceSection.tsx +166 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SortControl.tsx +62 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/config.json +83 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useSearch.ts +703 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/index.ts +90 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/loadConfig.ts +14 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts +156 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/types.ts +307 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/debounce.ts +17 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/fieldUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/filterUtils.ts +285 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/sortUtils.ts +34 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +49 -16
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +21 -293
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/Home.tsx +22 -7
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/utils/fieldUtils.ts → utils/accountFields.ts} +9 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/accountSearchService.ts +0 -46
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountIndustries.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountTypes.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/searchAccounts.graphql +0 -51
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/graphql-operations-types.ts +0 -11260
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/api/objectSearchService.ts +0 -82
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/PaginationControls.tsx +0 -109
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SearchBar.tsx +0 -41
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SortControl.tsx +0 -143
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/debounce.ts +0 -25
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/filterUtils.ts +0 -404
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/sortUtils.ts +0 -38
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the search feature.
|
|
3
|
+
*
|
|
4
|
+
* Most apps only need:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { Search, config } from ".../features/search";
|
|
7
|
+
* <Search config={config} />
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* For more control, drop down to primitives:
|
|
11
|
+
* - {@link useSearch} hook + {@link SearchBar} + {@link SearchResults}
|
|
12
|
+
* - {@link DefaultResultRow} / {@link DefaultFilterPanel} for opt-in defaults
|
|
13
|
+
* - filter components (TextFilter, SelectFilter, ...) for custom sidebars
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Drop-in wrapper (most common entry point)
|
|
17
|
+
export { Search } from "./components/Search";
|
|
18
|
+
|
|
19
|
+
// Primitives
|
|
20
|
+
export { useSearch } from "./hooks/useSearch";
|
|
21
|
+
export { useAsyncData } from "./hooks/useAsyncData";
|
|
22
|
+
export { useDistinctValues } from "./hooks/useDistinctValues";
|
|
23
|
+
|
|
24
|
+
export { SearchBar } from "./components/controls/SearchBar";
|
|
25
|
+
export { SearchResults } from "./components/SearchResults";
|
|
26
|
+
export { MergedSearchResults } from "./components/MergedSearchResults";
|
|
27
|
+
export type { MergedSearchResultsProps } from "./components/MergedSearchResults";
|
|
28
|
+
export { SourceSection } from "./components/SourceSection";
|
|
29
|
+
export { SortControl } from "./components/controls/SortControl";
|
|
30
|
+
export { PaginationControls } from "./components/controls/PaginationControls";
|
|
31
|
+
export { ScopeSelector } from "./components/controls/ScopeSelector";
|
|
32
|
+
export { ActiveFilters } from "./components/filters/ActiveFilters";
|
|
33
|
+
export { DefaultResultRow } from "./components/results/DefaultResultRow";
|
|
34
|
+
export { DefaultFilterPanel } from "./components/filters/DefaultFilterPanel";
|
|
35
|
+
export {
|
|
36
|
+
FilterProvider,
|
|
37
|
+
FilterResetButton,
|
|
38
|
+
useFilterField,
|
|
39
|
+
useFilterPanel,
|
|
40
|
+
} from "./components/filters/FilterContext";
|
|
41
|
+
|
|
42
|
+
export { TextFilter } from "./components/filters/inputs/TextFilter";
|
|
43
|
+
export { SelectFilter } from "./components/filters/inputs/SelectFilter";
|
|
44
|
+
export { MultiSelectFilter } from "./components/filters/inputs/MultiSelectFilter";
|
|
45
|
+
export { NumericRangeFilter } from "./components/filters/inputs/NumericRangeFilter";
|
|
46
|
+
export { BooleanFilter } from "./components/filters/inputs/BooleanFilter";
|
|
47
|
+
export { DateRangeFilter } from "./components/filters/inputs/DateRangeFilter";
|
|
48
|
+
export { FilterFieldWrapper } from "./components/filters/inputs/FilterFieldWrapper";
|
|
49
|
+
|
|
50
|
+
export { fieldValue } from "./utils/fieldUtils";
|
|
51
|
+
export {
|
|
52
|
+
buildFilter,
|
|
53
|
+
buildGlobalQueryClause,
|
|
54
|
+
readSourceParams,
|
|
55
|
+
writeSourceParams,
|
|
56
|
+
GLOBAL_QUERY_KEY,
|
|
57
|
+
} from "./utils/filterUtils";
|
|
58
|
+
export type {
|
|
59
|
+
ActiveFilterValue,
|
|
60
|
+
DateFilterMode,
|
|
61
|
+
FilterFieldConfig,
|
|
62
|
+
FilterFieldType,
|
|
63
|
+
} from "./utils/filterUtils";
|
|
64
|
+
export { buildOrderBy } from "./utils/sortUtils";
|
|
65
|
+
export type { SortFieldConfig, SortState } from "./utils/sortUtils";
|
|
66
|
+
|
|
67
|
+
export { buildSearchQuery } from "./queryBuilder";
|
|
68
|
+
export type { SourceRequest, SearchQueryPayload } from "./queryBuilder";
|
|
69
|
+
|
|
70
|
+
export { runSearch } from "./api/searchService";
|
|
71
|
+
export { fetchDistinctValues } from "./api/distinctValuesService";
|
|
72
|
+
export type { PicklistOption } from "./api/distinctValuesService";
|
|
73
|
+
|
|
74
|
+
export { config } from "./loadConfig";
|
|
75
|
+
|
|
76
|
+
export { ALL_SCOPE } from "./types";
|
|
77
|
+
export type {
|
|
78
|
+
DisplayField,
|
|
79
|
+
SObjectSourceConfig,
|
|
80
|
+
SearchConfig,
|
|
81
|
+
SearchPaginationConfig,
|
|
82
|
+
SourceController,
|
|
83
|
+
SourceResult,
|
|
84
|
+
SourcePageInfo,
|
|
85
|
+
SearchHandle,
|
|
86
|
+
SearchScope,
|
|
87
|
+
RenderResultFn,
|
|
88
|
+
GlobalPagination,
|
|
89
|
+
MergedResultItem,
|
|
90
|
+
} from "./types";
|
package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/loadConfig.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed re-export of the static config.json that ships with the feature.
|
|
3
|
+
*
|
|
4
|
+
* Application code:
|
|
5
|
+
* import { config } from ".../features/search/loadConfig";
|
|
6
|
+
* const handle = useSearch(config);
|
|
7
|
+
*
|
|
8
|
+
* To customize, edit `config.json` directly (no code changes required).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import rawConfig from "./config.json";
|
|
12
|
+
import type { SearchConfig } from "./types";
|
|
13
|
+
|
|
14
|
+
export const config: SearchConfig = rawConfig as SearchConfig;
|
package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a single multi-aliased GraphQL document for search.
|
|
3
|
+
*
|
|
4
|
+
* Each SObject source becomes one aliased child of `uiapi.query`, with its own
|
|
5
|
+
* `first / after / where / orderBy` variables. Variable names are
|
|
6
|
+
* `${sourceKey}_first` etc. so two sources with similar names never collide.
|
|
7
|
+
*
|
|
8
|
+
* Selection sets are generated from `displayFields`:
|
|
9
|
+
* - string `"Name"` → `Name @optional { value displayValue }`
|
|
10
|
+
* - `{ name, raw: true }` → `Name`
|
|
11
|
+
* - `{ name, subfields }` → nested `Name @optional { ... }`
|
|
12
|
+
*
|
|
13
|
+
* The `idField` (default "Id") is always emitted; it is **not** wrapped
|
|
14
|
+
* because uiapi's Id is a scalar.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { buildFilter, buildGlobalQueryClause, type ActiveFilterValue } from "./utils/filterUtils";
|
|
18
|
+
import { buildOrderBy, type SortState } from "./utils/sortUtils";
|
|
19
|
+
import type { DisplayField, SObjectSourceConfig } from "./types";
|
|
20
|
+
|
|
21
|
+
const KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
|
+
|
|
23
|
+
function assertValidKey(key: string): void {
|
|
24
|
+
if (!KEY_PATTERN.test(key)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid source key "${key}". Must match /^[A-Za-z_][A-Za-z0-9_]*$/ to be a valid GraphQL alias and variable prefix.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SearchQueryPayload {
|
|
32
|
+
document: string;
|
|
33
|
+
variables: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SourceRequest {
|
|
37
|
+
source: SObjectSourceConfig;
|
|
38
|
+
q: string;
|
|
39
|
+
filters: ActiveFilterValue[];
|
|
40
|
+
sort: SortState | null;
|
|
41
|
+
pageSize: number;
|
|
42
|
+
afterCursor: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Combines a per-source structured-filter clause with the global-q `or` clause.
|
|
47
|
+
* Returns `undefined` when neither side has any constraints.
|
|
48
|
+
*/
|
|
49
|
+
function buildSourceWhere(
|
|
50
|
+
source: SObjectSourceConfig,
|
|
51
|
+
q: string,
|
|
52
|
+
filters: ActiveFilterValue[],
|
|
53
|
+
): unknown {
|
|
54
|
+
const clauses: unknown[] = [];
|
|
55
|
+
const globalClause = buildGlobalQueryClause(q, source.searchableFields);
|
|
56
|
+
if (globalClause) clauses.push(globalClause);
|
|
57
|
+
const structured = buildFilter(filters, source.filterBy ?? []);
|
|
58
|
+
if (structured) clauses.push(structured);
|
|
59
|
+
if (clauses.length === 0) return undefined;
|
|
60
|
+
if (clauses.length === 1) return clauses[0];
|
|
61
|
+
return { and: clauses };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Builds the multi-aliased query document and a flat variables map. */
|
|
65
|
+
export function buildSearchQuery(requests: SourceRequest[]): SearchQueryPayload {
|
|
66
|
+
if (requests.length === 0) {
|
|
67
|
+
throw new Error("buildSearchQuery requires at least one source request");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const variableDeclarations: string[] = [];
|
|
71
|
+
const querySelections: string[] = [];
|
|
72
|
+
const variables: Record<string, unknown> = {};
|
|
73
|
+
|
|
74
|
+
for (const request of requests) {
|
|
75
|
+
const { source } = request;
|
|
76
|
+
assertValidKey(source.key);
|
|
77
|
+
|
|
78
|
+
const firstVar = `${source.key}_first`;
|
|
79
|
+
const afterVar = `${source.key}_after`;
|
|
80
|
+
const whereVar = `${source.key}_where`;
|
|
81
|
+
const orderByVar = `${source.key}_orderBy`;
|
|
82
|
+
const whereType = source.whereTypeName ?? `${source.objectName}_Filter`;
|
|
83
|
+
const orderByType = source.orderByTypeName ?? `${source.objectName}_OrderBy`;
|
|
84
|
+
|
|
85
|
+
variableDeclarations.push(
|
|
86
|
+
`$${firstVar}: Int`,
|
|
87
|
+
`$${afterVar}: String`,
|
|
88
|
+
`$${whereVar}: ${whereType}`,
|
|
89
|
+
`$${orderByVar}: ${orderByType}`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
querySelections.push(
|
|
93
|
+
`${source.key}: ${source.objectName}(` +
|
|
94
|
+
`first: $${firstVar}, ` +
|
|
95
|
+
`after: $${afterVar}, ` +
|
|
96
|
+
`where: $${whereVar}, ` +
|
|
97
|
+
`orderBy: $${orderByVar}` +
|
|
98
|
+
`) {\n` +
|
|
99
|
+
buildSelectionBody(source) +
|
|
100
|
+
`\n}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
variables[firstVar] = request.pageSize;
|
|
104
|
+
variables[afterVar] = request.afterCursor ?? null;
|
|
105
|
+
variables[whereVar] = buildSourceWhere(source, request.q, request.filters) ?? null;
|
|
106
|
+
variables[orderByVar] = buildOrderBy(request.sort) ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const document =
|
|
110
|
+
`query Search(${variableDeclarations.join(", ")}) {\n` +
|
|
111
|
+
` uiapi {\n` +
|
|
112
|
+
` query {\n` +
|
|
113
|
+
querySelections.map((s) => indent(s, " ")).join("\n") +
|
|
114
|
+
`\n }\n` +
|
|
115
|
+
` }\n` +
|
|
116
|
+
`}\n`;
|
|
117
|
+
|
|
118
|
+
return { document, variables };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildSelectionBody(source: SObjectSourceConfig): string {
|
|
122
|
+
const idField = source.idField ?? "Id";
|
|
123
|
+
const fieldLines = [` ${idField}`];
|
|
124
|
+
for (const field of source.displayFields) {
|
|
125
|
+
fieldLines.push(...renderField(field, " "));
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
` edges { node {`,
|
|
129
|
+
...fieldLines,
|
|
130
|
+
` } }`,
|
|
131
|
+
` pageInfo { hasNextPage hasPreviousPage startCursor endCursor }`,
|
|
132
|
+
` totalCount`,
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderField(field: DisplayField, indentStr: string): string[] {
|
|
137
|
+
if (typeof field === "string") {
|
|
138
|
+
return [`${indentStr}${field} @optional { value displayValue }`];
|
|
139
|
+
}
|
|
140
|
+
if ("raw" in field) {
|
|
141
|
+
return [`${indentStr}${field.name}`];
|
|
142
|
+
}
|
|
143
|
+
const lines = [`${indentStr}${field.name} @optional {`];
|
|
144
|
+
for (const sub of field.subfields) {
|
|
145
|
+
lines.push(...renderField(sub, indentStr + " "));
|
|
146
|
+
}
|
|
147
|
+
lines.push(`${indentStr}}`);
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function indent(s: string, prefix: string): string {
|
|
152
|
+
return s
|
|
153
|
+
.split("\n")
|
|
154
|
+
.map((line) => prefix + line)
|
|
155
|
+
.join("\n");
|
|
156
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types for unified (multi-source) search.
|
|
3
|
+
*
|
|
4
|
+
* The application developer declares a SearchConfig with one entry per
|
|
5
|
+
* searchable backend. Today every source is an SObject queried via the uiapi
|
|
6
|
+
* GraphQL bridge; future adapters (CMS, REST, etc.) can plug in by introducing
|
|
7
|
+
* a new `kind` and a matching runtime adapter without changing any consumer.
|
|
8
|
+
*
|
|
9
|
+
* The library handles query construction, filter/sort/pagination state, URL
|
|
10
|
+
* sync, and result fan-out. The application owns rendering and routing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FilterFieldConfig, ActiveFilterValue } from "./utils/filterUtils";
|
|
14
|
+
import type { SortFieldConfig, SortState } from "./utils/sortUtils";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single field to include in the GraphQL selection set of an SObject source.
|
|
18
|
+
*
|
|
19
|
+
* - `"Name"` (string shorthand): emits `Name @optional { value displayValue }`.
|
|
20
|
+
* - `{ name, subfields }`: emits a nested selection (relationship traversal).
|
|
21
|
+
* - `{ name, raw: true }`: emits the bare field name (e.g. for Id-like fields
|
|
22
|
+
* that are not wrapped in `{ value displayValue }`).
|
|
23
|
+
*
|
|
24
|
+
* The configured `idField` (default: "Id") is added automatically; do not list
|
|
25
|
+
* it explicitly.
|
|
26
|
+
*/
|
|
27
|
+
export type DisplayField =
|
|
28
|
+
| string
|
|
29
|
+
| { name: string; raw: true }
|
|
30
|
+
| { name: string; subfields: DisplayField[] };
|
|
31
|
+
|
|
32
|
+
/** Configuration for one SObject source in a search experience. */
|
|
33
|
+
export interface SObjectSourceConfig {
|
|
34
|
+
/** Discriminator — selects the runtime adapter. */
|
|
35
|
+
kind: "sobject";
|
|
36
|
+
/**
|
|
37
|
+
* Stable identifier. Used as the GraphQL alias, the URL namespace, and the
|
|
38
|
+
* result-map key. Must match `/^[A-Za-z_][A-Za-z0-9_]*$/`.
|
|
39
|
+
*/
|
|
40
|
+
key: string;
|
|
41
|
+
/** GraphQL object name (e.g. "Account", "Contact"). */
|
|
42
|
+
objectName: string;
|
|
43
|
+
/**
|
|
44
|
+
* Plural display label — used in section headers, the scope dropdown, and
|
|
45
|
+
* empty-state messages (e.g. "Accounts", "Maintenance Requests").
|
|
46
|
+
*/
|
|
47
|
+
label: string;
|
|
48
|
+
/**
|
|
49
|
+
* Singular display label for a single record of this type (e.g. "Account",
|
|
50
|
+
* "Maintenance Request"). Used for the per-card source badge in the merged
|
|
51
|
+
* grid. Falls back to `label` when omitted.
|
|
52
|
+
*/
|
|
53
|
+
labelSingular?: string;
|
|
54
|
+
/** Field name that uniquely identifies a record. Defaults to "Id". */
|
|
55
|
+
idField?: string;
|
|
56
|
+
/**
|
|
57
|
+
* URL pattern used by the default renderer to build a result link.
|
|
58
|
+
*
|
|
59
|
+
* Tokens of the form `:fieldName` are replaced with the corresponding
|
|
60
|
+
* field's value at render time (e.g. `:id` resolves to the record's
|
|
61
|
+
* `idField` value, `:Name` resolves to the Name field's `value`).
|
|
62
|
+
*
|
|
63
|
+
* Example: `"/accounts/:id"` produces `<Link to="/accounts/001xx0000003DGb">`.
|
|
64
|
+
*
|
|
65
|
+
* Optional — when omitted, the default renderer emits non-clickable rows.
|
|
66
|
+
*/
|
|
67
|
+
routePattern?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Fields the global `q` term should match against.
|
|
70
|
+
* Built into an `or` of `like %q%`. Supports dot-notation for relationship
|
|
71
|
+
* fields (e.g. "Owner.Name").
|
|
72
|
+
*/
|
|
73
|
+
searchableFields: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Fields to include in the GraphQL selection set. The id field is added
|
|
76
|
+
* automatically; do not list it.
|
|
77
|
+
*/
|
|
78
|
+
displayFields: DisplayField[];
|
|
79
|
+
/**
|
|
80
|
+
* Optional structured filters for this source.
|
|
81
|
+
*
|
|
82
|
+
* Named `filterBy` (rather than "the filters shown in the panel") because a
|
|
83
|
+
* future entry may constrain the query without rendering a control — e.g. a
|
|
84
|
+
* default `Status != Completed` that hides completed items from results while
|
|
85
|
+
* never appearing in the UI. Today every entry renders an input via
|
|
86
|
+
* `DefaultFilterPanel`.
|
|
87
|
+
*/
|
|
88
|
+
filterBy?: FilterFieldConfig[];
|
|
89
|
+
/** Optional sort options surfaced in the per-source sort dropdown. */
|
|
90
|
+
sortBy?: SortFieldConfig[];
|
|
91
|
+
/** Initial sort applied when no URL sort is present. */
|
|
92
|
+
defaultSort?: SortState;
|
|
93
|
+
/**
|
|
94
|
+
* GraphQL type name for the `where` variable.
|
|
95
|
+
* Defaults to `${objectName}_Filter` — override only for non-conventional schemas.
|
|
96
|
+
*/
|
|
97
|
+
whereTypeName?: string;
|
|
98
|
+
/** GraphQL type name for the `orderBy` variable. Defaults to `${objectName}_OrderBy`. */
|
|
99
|
+
orderByTypeName?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One global pagination config for the whole search experience. It is the
|
|
104
|
+
* single source of truth — there is no per-source pagination. The same
|
|
105
|
+
* `pageSize` / `pageSizeOptions` apply across every object type and in every
|
|
106
|
+
* scope, including single-object pages (`restrictTo` / `lockedScope`) and when
|
|
107
|
+
* the scope dropdown narrows to one source.
|
|
108
|
+
*/
|
|
109
|
+
export interface SearchPaginationConfig {
|
|
110
|
+
/**
|
|
111
|
+
* How results are paginated:
|
|
112
|
+
* - `"per-source"` (default): each source advances its own cursor; a page
|
|
113
|
+
* can hold up to `sources × pageSize` items. This is what the drop-in
|
|
114
|
+
* `<Search>` (one section per source) expects.
|
|
115
|
+
* - `"merged"`: treat the cumulative result set as one list and window it
|
|
116
|
+
* into fixed pages of `pageSize`. Each page shows exactly `pageSize` items
|
|
117
|
+
* across all sources — use for a single combined grid.
|
|
118
|
+
*/
|
|
119
|
+
mode?: "per-source" | "merged";
|
|
120
|
+
/**
|
|
121
|
+
* In `"merged"` mode, how nodes from different sources are ordered:
|
|
122
|
+
* - `"sequential"` (default): source 1 in full, then source 2, …
|
|
123
|
+
* - `"interleaved"`: round-robin — every source gets equal priority.
|
|
124
|
+
* - `"proportional"`: each source spread by its result count, so larger
|
|
125
|
+
* sources appear more often. Ignored in `"per-source"` mode.
|
|
126
|
+
*/
|
|
127
|
+
mergeOrder?: "sequential" | "interleaved" | "proportional";
|
|
128
|
+
/** Items per page (across all sources in `"merged"` mode; per source otherwise). */
|
|
129
|
+
pageSize: number;
|
|
130
|
+
/** Page sizes offered in the "results per page" control. */
|
|
131
|
+
pageSizeOptions: number[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Top-level config passed to {@link useSearch}. */
|
|
135
|
+
export interface SearchConfig {
|
|
136
|
+
sources: SObjectSourceConfig[];
|
|
137
|
+
/**
|
|
138
|
+
* Global pagination config — the single source of truth for page size, page
|
|
139
|
+
* options, mode, and merge order. See {@link SearchPaginationConfig}. When
|
|
140
|
+
* omitted, pagination defaults to per-source mode, page size 10, options
|
|
141
|
+
* `[10, 25, 50]`.
|
|
142
|
+
*/
|
|
143
|
+
pagination?: SearchPaginationConfig;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Runtime types
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export interface SourcePageInfo {
|
|
151
|
+
hasNextPage: boolean;
|
|
152
|
+
hasPreviousPage: boolean;
|
|
153
|
+
endCursor: string | null;
|
|
154
|
+
startCursor: string | null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Per-source result. Nodes are typed as `unknown` at the library boundary —
|
|
159
|
+
* the application narrows them in its `renderResult` callback (typically by
|
|
160
|
+
* casting to a codegen-generated node type).
|
|
161
|
+
*/
|
|
162
|
+
export interface SourceResult {
|
|
163
|
+
nodes: unknown[];
|
|
164
|
+
pageInfo: SourcePageInfo | null;
|
|
165
|
+
totalCount: number | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface SourceController {
|
|
169
|
+
config: SObjectSourceConfig;
|
|
170
|
+
result: SourceResult | null;
|
|
171
|
+
loading: boolean;
|
|
172
|
+
error: string | null;
|
|
173
|
+
filters: {
|
|
174
|
+
active: ActiveFilterValue[];
|
|
175
|
+
set: (field: string, value: ActiveFilterValue | undefined) => void;
|
|
176
|
+
remove: (field: string) => void;
|
|
177
|
+
};
|
|
178
|
+
sort: {
|
|
179
|
+
current: SortState | null;
|
|
180
|
+
set: (sort: SortState | null) => void;
|
|
181
|
+
};
|
|
182
|
+
pagination: {
|
|
183
|
+
pageSize: number;
|
|
184
|
+
/** Allowed page sizes — from the global pagination config. */
|
|
185
|
+
pageSizeOptions: readonly number[];
|
|
186
|
+
pageIndex: number;
|
|
187
|
+
hasNextPage: boolean;
|
|
188
|
+
hasPreviousPage: boolean;
|
|
189
|
+
setPageSize: (size: number) => void;
|
|
190
|
+
goToNextPage: () => void;
|
|
191
|
+
goToPreviousPage: () => void;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The currently-selected search scope.
|
|
197
|
+
*
|
|
198
|
+
* - `"all"`: every source in the config is queried and rendered.
|
|
199
|
+
* - any source `key`: only that source is queried and rendered. Other sources
|
|
200
|
+
* skip both the network request and the UI section.
|
|
201
|
+
*/
|
|
202
|
+
export type SearchScope = "all" | string;
|
|
203
|
+
|
|
204
|
+
export const ALL_SCOPE = "all" as const;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* One result node tagged with the source it came from. Produced by
|
|
208
|
+
* {@link SearchHandle.mergedResults} so a single combined grid can render
|
|
209
|
+
* cross-source results and still pick the right card per node.
|
|
210
|
+
*/
|
|
211
|
+
export interface MergedResultItem {
|
|
212
|
+
/** The `key` of the source this node came from. */
|
|
213
|
+
sourceKey: string;
|
|
214
|
+
/** The raw result node (cast to your typed node in the renderer). */
|
|
215
|
+
node: unknown;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Aggregate pagination across every in-scope source, exposed by
|
|
220
|
+
* {@link SearchHandle.globalPagination}. Its exact behaviour depends on the
|
|
221
|
+
* `pagination` mode passed to {@link useSearch}:
|
|
222
|
+
*
|
|
223
|
+
* - **`"merged"`** (recommended for a single combined grid): the cumulative
|
|
224
|
+
* result set is treated as one list and windowed into fixed pages of
|
|
225
|
+
* `pageSize`. Page `P` always shows **exactly** `pageSize` items (the last
|
|
226
|
+
* page may show fewer), `pageCount` is `ceil(totalCount / pageSize)`, and
|
|
227
|
+
* `goToPage` can jump to any page. Under the hood each in-scope source fetches
|
|
228
|
+
* `(P + 1) * pageSize` rows from its front, the results are concatenated in
|
|
229
|
+
* config order, and the page-`P` slice is taken — so the math is exact, not an
|
|
230
|
+
* estimate.
|
|
231
|
+
* - **`"per-source"`** (default): each source paginates with its own cursor and
|
|
232
|
+
* a "global page" is the furthest-advanced in-scope source's page. `Next`
|
|
233
|
+
* advances only the sources still on the current global page that have more
|
|
234
|
+
* data; a source that runs out is left behind. `goToPage` is best-effort
|
|
235
|
+
* (adjacent pages only) because cursors can't seek to an arbitrary offset.
|
|
236
|
+
*/
|
|
237
|
+
export interface GlobalPagination {
|
|
238
|
+
/** Current global page, 0-based. */
|
|
239
|
+
pageIndex: number;
|
|
240
|
+
/**
|
|
241
|
+
* Total number of global pages. In `"merged"` mode this is exactly
|
|
242
|
+
* `ceil(totalCount / pageSize)`. In `"per-source"` mode it is the `max` over
|
|
243
|
+
* in-scope sources of `ceil(totalCount / pageSize)` and may be an estimate if
|
|
244
|
+
* a source omits `totalCount`. `0` when there are no results, otherwise `>= 1`.
|
|
245
|
+
*/
|
|
246
|
+
pageCount: number;
|
|
247
|
+
/** Shared page size. */
|
|
248
|
+
pageSize: number;
|
|
249
|
+
/** Allowed page sizes (from the first in-scope source). */
|
|
250
|
+
pageSizeOptions: readonly number[];
|
|
251
|
+
/** True when there is a page after the current one. */
|
|
252
|
+
hasNextPage: boolean;
|
|
253
|
+
/** True when the current global page is past the first. */
|
|
254
|
+
hasPreviousPage: boolean;
|
|
255
|
+
/** Cumulative result count summed across in-scope sources. */
|
|
256
|
+
totalCount: number;
|
|
257
|
+
/** Advance to the next global page. */
|
|
258
|
+
goToNextPage: () => void;
|
|
259
|
+
/** Step back to the previous global page. */
|
|
260
|
+
goToPreviousPage: () => void;
|
|
261
|
+
/**
|
|
262
|
+
* Jump to a specific 0-based page. Exact in `"merged"` mode; in
|
|
263
|
+
* `"per-source"` mode only adjacent pages are honoured (others are a no-op).
|
|
264
|
+
*/
|
|
265
|
+
goToPage: (pageIndex: number) => void;
|
|
266
|
+
/** Set the page size (resets to the first page). */
|
|
267
|
+
setPageSize: (size: number) => void;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface SearchHandle {
|
|
271
|
+
q: string;
|
|
272
|
+
setQ: (q: string) => void;
|
|
273
|
+
scope: SearchScope;
|
|
274
|
+
setScope: (scope: SearchScope) => void;
|
|
275
|
+
/**
|
|
276
|
+
* True when the caller passed `lockedScope` to `useSearch`. Consumers
|
|
277
|
+
* should hide the scope dropdown and read-only display the locked source.
|
|
278
|
+
*/
|
|
279
|
+
scopeLocked: boolean;
|
|
280
|
+
sources: Record<string, SourceController>;
|
|
281
|
+
/**
|
|
282
|
+
* Source controllers that participate in the current scope, in config
|
|
283
|
+
* declaration order. In `"all"` scope this is every source; otherwise just
|
|
284
|
+
* the selected one. Convenience for callers building aggregate UI.
|
|
285
|
+
*/
|
|
286
|
+
inScopeSources: SourceController[];
|
|
287
|
+
/**
|
|
288
|
+
* In-scope nodes flattened into one list (config order, source by source),
|
|
289
|
+
* each tagged with its `sourceKey` — for rendering a single combined grid
|
|
290
|
+
* instead of one section per source. In `"merged"` pagination mode this is
|
|
291
|
+
* exactly the current page's window (at most `globalPagination.pageSize`
|
|
292
|
+
* items); in `"per-source"` mode it is every in-scope source's current page
|
|
293
|
+
* concatenated.
|
|
294
|
+
*/
|
|
295
|
+
mergedResults: MergedResultItem[];
|
|
296
|
+
/**
|
|
297
|
+
* Aggregate pagination across all in-scope sources — drive one shared
|
|
298
|
+
* pager over the cumulative result set. See {@link GlobalPagination}.
|
|
299
|
+
*/
|
|
300
|
+
globalPagination: GlobalPagination;
|
|
301
|
+
loading: boolean;
|
|
302
|
+
error: string | null;
|
|
303
|
+
resetAll: () => void;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Type alias for a render callback per source key. */
|
|
307
|
+
export type RenderResultFn = (sourceKey: string, node: unknown) => React.ReactNode;
|
package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/debounce.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Default debounce delay for keystroke-driven inputs. */
|
|
2
|
+
export const FILTER_DEBOUNCE_MS = 300;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a debounced version of `fn`. Each call resets the internal timer;
|
|
6
|
+
* `fn` runs only after the timer expires without being reset.
|
|
7
|
+
*/
|
|
8
|
+
export function debounce<T extends (...args: any[]) => void>(
|
|
9
|
+
fn: T,
|
|
10
|
+
ms: number,
|
|
11
|
+
): (...args: Parameters<T>) => void {
|
|
12
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
+
return (...args: Parameters<T>) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for reading values out of uiapi GraphQL response nodes.
|
|
3
|
+
*
|
|
4
|
+
* Nodes typically come back wrapped as `{ value, displayValue }`. `fieldValue`
|
|
5
|
+
* picks the user-facing string (preferring displayValue) or returns null.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function fieldValue(
|
|
9
|
+
field: { displayValue?: string | null; value?: unknown } | null | undefined,
|
|
10
|
+
): string | null {
|
|
11
|
+
if (field?.displayValue != null) return field.displayValue;
|
|
12
|
+
if (field?.value != null) return String(field.value);
|
|
13
|
+
return null;
|
|
14
|
+
}
|