@salesforce/ui-bundle-template-app-react-template-b2e 11.55.0 → 11.56.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/reactinternalapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/README.md +235 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/api/apiUtils.ts +56 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/api/orgApiVersionService.ts +26 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/api/searchableContentTypesService.ts +116 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/channelResolver.ts +40 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/cmsQueryFragment.ts +97 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/contentTypeSessionCache.ts +134 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/contentTypeUtils.ts +38 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/hooks/useSearchableContentTypes.ts +128 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/index.ts +54 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/parseResponse.ts +65 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/searchChannel.ts +15 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/cms/types.ts +66 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/registry.ts +38 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/sobject/index.ts +19 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/sobject/parseResponse.ts +56 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/sobject/queryFragment.ts +141 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/adapters/types.ts +101 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/api/searchService.ts +110 -45
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/GlobalSearchBox.tsx +98 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/MergedSearchResults.tsx +34 -17
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/Search.tsx +31 -13
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/SearchResults.tsx +17 -11
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/SourceSection.tsx +9 -4
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/controls/ScopeSelector.tsx +50 -6
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/results/CmsResultRow.tsx +101 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/components/results/resolveResultRenderer.ts +49 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/config.json +7 -2
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/constants.ts +8 -0
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/hooks/useSearch.ts +313 -50
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/index.ts +26 -1
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/queryBuilder.ts +62 -118
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/types.ts +74 -5
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/Home.tsx +3 -42
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/routes.tsx +12 -5
- package/dist/force-app/main/default/uiBundles/reactinternalapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +4 -2
- package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/AccountSearch.tsx +0 -25
package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/search/queryBuilder.ts
CHANGED
|
@@ -1,151 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds a single
|
|
2
|
+
* Builds a single combined GraphQL document for search across every source.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The builder is kind-agnostic: for each source request it asks the registered
|
|
5
|
+
* adapter (`getAdapter(kind).buildRequest`) for a `QueryFragmentContribution`
|
|
6
|
+
* and assembles them:
|
|
7
|
+
* - `placement: "uiapi.query"` fragments (e.g. SObject sources) go inside the
|
|
8
|
+
* shared `uiapi { query { … } }` block.
|
|
9
|
+
* - `placement: "root"` fragments (e.g. the CMS `managed_content` block) go in
|
|
10
|
+
* as siblings of `uiapi` at the document root.
|
|
11
|
+
* - every contribution's `variableDeclarations` are concatenated into the
|
|
12
|
+
* `query Search(...)` header and every contribution's `variables` merged
|
|
13
|
+
* into one flat map.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - `{ name, subfields }` → nested `Name @optional { ... }`
|
|
15
|
+
* A contribution may declare zero extra variables (the CMS bootstrap call omits
|
|
16
|
+
* `$cmsContentTypeFQNs` entirely), so the merge tolerates declaration lists of
|
|
17
|
+
* differing lengths and never emits a declared-but-unused variable.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* When only SObject sources participate, the emitted document is identical to
|
|
20
|
+
* the pre-adapter builder — the `uiapi` block is unchanged and no root siblings
|
|
21
|
+
* are added.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import type { DisplayField, SObjectSourceConfig } from "./types";
|
|
24
|
+
import { getAdapter } from "./adapters/registry";
|
|
25
|
+
import type { SourceRequest } from "./adapters/types";
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
}
|
|
27
|
+
// `SourceRequest` is now defined canonically in `adapters/types.ts`; re-export
|
|
28
|
+
// it here for backward compatibility (the barrel and existing consumers import
|
|
29
|
+
// `SourceRequest` from `./queryBuilder`).
|
|
30
|
+
export type { SourceRequest } from "./adapters/types";
|
|
30
31
|
|
|
31
32
|
export interface SearchQueryPayload {
|
|
32
33
|
document: string;
|
|
33
34
|
variables: Record<string, unknown>;
|
|
34
35
|
}
|
|
35
36
|
|
|
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
37
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
38
|
+
* Builds the combined query document and a flat variables map by dispatching
|
|
39
|
+
* each source through its adapter and partitioning fragments by placement.
|
|
48
40
|
*/
|
|
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
41
|
export function buildSearchQuery(requests: SourceRequest[]): SearchQueryPayload {
|
|
66
42
|
if (requests.length === 0) {
|
|
67
43
|
throw new Error("buildSearchQuery requires at least one source request");
|
|
68
44
|
}
|
|
69
45
|
|
|
70
46
|
const variableDeclarations: string[] = [];
|
|
71
|
-
const
|
|
47
|
+
const uiapiFragments: string[] = [];
|
|
48
|
+
const rootFragments: string[] = [];
|
|
72
49
|
const variables: Record<string, unknown> = {};
|
|
73
50
|
|
|
74
51
|
for (const request of requests) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
);
|
|
52
|
+
const contribution = getAdapter(request.source.kind).buildRequest(request);
|
|
53
|
+
|
|
54
|
+
// Concatenate declarations (may be empty for a given source) and merge
|
|
55
|
+
// values — an omitted declaration means the variable is never declared,
|
|
56
|
+
// so it is likewise never supplied.
|
|
57
|
+
variableDeclarations.push(...contribution.variableDeclarations);
|
|
58
|
+
Object.assign(variables, contribution.variables);
|
|
59
|
+
|
|
60
|
+
if (contribution.placement === "root") {
|
|
61
|
+
rootFragments.push(contribution.fragment);
|
|
62
|
+
} else {
|
|
63
|
+
uiapiFragments.push(contribution.fragment);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
102
66
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
67
|
+
const parts: string[] = [`query Search(${variableDeclarations.join(", ")}) {`];
|
|
68
|
+
|
|
69
|
+
// The `uiapi { query { … } }` block holds every `uiapi.query`-placed fragment.
|
|
70
|
+
// Emit it only when at least one such fragment exists — a CMS-only request
|
|
71
|
+
// (all SObject sources dropped) must not produce an empty, invalid selection
|
|
72
|
+
// set. When SObject sources are present its shape is identical to the
|
|
73
|
+
// pre-adapter builder.
|
|
74
|
+
if (uiapiFragments.length > 0) {
|
|
75
|
+
parts.push(` uiapi {`);
|
|
76
|
+
parts.push(` query {`);
|
|
77
|
+
parts.push(uiapiFragments.map((s) => indent(s, " ")).join("\n"));
|
|
78
|
+
parts.push(` }`);
|
|
79
|
+
parts.push(` }`);
|
|
107
80
|
}
|
|
108
81
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
querySelections.map((s) => indent(s, " ")).join("\n") +
|
|
114
|
-
`\n }\n` +
|
|
115
|
-
` }\n` +
|
|
116
|
-
`}\n`;
|
|
82
|
+
// Root-placement fragments (e.g. CMS `managed_content`) are siblings of uiapi.
|
|
83
|
+
for (const fragment of rootFragments) {
|
|
84
|
+
parts.push(indent(fragment, " "));
|
|
85
|
+
}
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
}
|
|
87
|
+
parts.push(`}`);
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
const
|
|
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
|
-
}
|
|
89
|
+
// Trailing newline preserved from the original builder.
|
|
90
|
+
const document = parts.join("\n") + "\n";
|
|
135
91
|
|
|
136
|
-
|
|
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;
|
|
92
|
+
return { document, variables };
|
|
149
93
|
}
|
|
150
94
|
|
|
151
95
|
function indent(s: string, prefix: string): string {
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import type { FilterFieldConfig, ActiveFilterValue } from "./utils/filterUtils";
|
|
14
14
|
import type { SortFieldConfig, SortState } from "./utils/sortUtils";
|
|
15
|
+
import type { CmsSourceConfig } from "./adapters/cms/types";
|
|
16
|
+
import type { DiscoveredContentType } from "./adapters/cms/contentTypeUtils";
|
|
17
|
+
|
|
18
|
+
// Re-export the CMS source config so consumers can import it from the public
|
|
19
|
+
// type surface without reaching into `adapters/cms`.
|
|
20
|
+
export type { CmsSourceConfig };
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* A single field to include in the GraphQL selection set of an SObject source.
|
|
@@ -99,6 +105,18 @@ export interface SObjectSourceConfig {
|
|
|
99
105
|
orderByTypeName?: string;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Discriminated union of every source kind a search experience can query.
|
|
110
|
+
*
|
|
111
|
+
* The `kind` field selects the runtime adapter (see `adapters/registry.ts`) and
|
|
112
|
+
* narrows the config to the fields that kind understands. New backends extend
|
|
113
|
+
* this union with a new `kind` + a matching adapter — no consumer changes.
|
|
114
|
+
*
|
|
115
|
+
* `CmsSourceConfig` is authored in `adapters/cms/types.ts` and unioned here so
|
|
116
|
+
* `SourceConfig` remains the single kind-agnostic config type.
|
|
117
|
+
*/
|
|
118
|
+
export type SourceConfig = SObjectSourceConfig | CmsSourceConfig;
|
|
119
|
+
|
|
102
120
|
/**
|
|
103
121
|
* One global pagination config for the whole search experience. It is the
|
|
104
122
|
* single source of truth — there is no per-source pagination. The same
|
|
@@ -133,7 +151,7 @@ export interface SearchPaginationConfig {
|
|
|
133
151
|
|
|
134
152
|
/** Top-level config passed to {@link useSearch}. */
|
|
135
153
|
export interface SearchConfig {
|
|
136
|
-
sources:
|
|
154
|
+
sources: SourceConfig[];
|
|
137
155
|
/**
|
|
138
156
|
* Global pagination config — the single source of truth for page size, page
|
|
139
157
|
* options, mode, and merge order. See {@link SearchPaginationConfig}. When
|
|
@@ -166,7 +184,12 @@ export interface SourceResult {
|
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
export interface SourceController {
|
|
169
|
-
|
|
187
|
+
/**
|
|
188
|
+
* The source's config. Widened to the `SourceConfig` union so a controller
|
|
189
|
+
* can front any kind (SObject, CMS, …); consumers narrow on `config.kind`
|
|
190
|
+
* before reading kind-specific fields (`objectName`, `filterBy`, `sortBy`, …).
|
|
191
|
+
*/
|
|
192
|
+
config: SourceConfig;
|
|
170
193
|
result: SourceResult | null;
|
|
171
194
|
loading: boolean;
|
|
172
195
|
error: string | null;
|
|
@@ -193,16 +216,53 @@ export interface SourceController {
|
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
/**
|
|
196
|
-
* The currently-selected search scope.
|
|
219
|
+
* The currently-selected search scope. Grammar:
|
|
220
|
+
*
|
|
221
|
+
* ```
|
|
222
|
+
* scope = "all" | <sobjectKey> | <cmsKey> | "<cmsKey>:<contentTypeFqn>"
|
|
223
|
+
* ```
|
|
197
224
|
*
|
|
198
225
|
* - `"all"`: every source in the config is queried and rendered.
|
|
199
|
-
* -
|
|
200
|
-
*
|
|
226
|
+
* - `<sobjectKey>` / `<cmsKey>`: only that source is queried and rendered; the
|
|
227
|
+
* combined query still runs (a CMS source in scope sends its discovered
|
|
228
|
+
* content types if any are known, else omits them on the bootstrap call).
|
|
229
|
+
* - `"<cmsKey>:<contentTypeFqn>"`: a **CMS-only** search narrowed to one content
|
|
230
|
+
* type — SObject sources are dropped from the request and the CMS fragment
|
|
231
|
+
* sends `contentTypes = [<fqn>]`. `<fqn>` must pass `isValidCmsFqn`. These
|
|
232
|
+
* entries are appended to the scope dropdown after content-type discovery.
|
|
233
|
+
*
|
|
234
|
+
* The `":"` separator disambiguates the two halves; source keys match
|
|
235
|
+
* `/^[A-Za-z_][A-Za-z0-9_]*$/` (no colon), and FQNs match `CMS_FQN_PATTERN`.
|
|
201
236
|
*/
|
|
202
237
|
export type SearchScope = "all" | string;
|
|
203
238
|
|
|
204
239
|
export const ALL_SCOPE = "all" as const;
|
|
205
240
|
|
|
241
|
+
/** Separator between a CMS source key and a content-type FQN in a scope value. */
|
|
242
|
+
export const CMS_SCOPE_SEPARATOR = ":" as const;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A parsed {@link SearchScope}. `base` is the plain scope (`"all"` or a source
|
|
246
|
+
* key); `cmsFqn` is the selected CMS content-type FQN when the scope carries the
|
|
247
|
+
* `"<cmsKey>:<fqn>"` form, else `undefined`.
|
|
248
|
+
*/
|
|
249
|
+
export interface ParsedScope {
|
|
250
|
+
base: SearchScope;
|
|
251
|
+
cmsFqn?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Splits a {@link SearchScope} into `{ base, cmsFqn }`. Only the first `":"`
|
|
256
|
+
* separates the CMS source key from the content-type FQN; source keys contain
|
|
257
|
+
* no colon and FQNs (`sfdc_cms__…`) contain no colon either, so a single split
|
|
258
|
+
* is unambiguous. A scope with no separator has `cmsFqn === undefined`.
|
|
259
|
+
*/
|
|
260
|
+
export function parseScope(scope: SearchScope): ParsedScope {
|
|
261
|
+
const sep = scope.indexOf(CMS_SCOPE_SEPARATOR);
|
|
262
|
+
if (sep === -1) return { base: scope };
|
|
263
|
+
return { base: scope.slice(0, sep), cmsFqn: scope.slice(sep + 1) };
|
|
264
|
+
}
|
|
265
|
+
|
|
206
266
|
/**
|
|
207
267
|
* One result node tagged with the source it came from. Produced by
|
|
208
268
|
* {@link SearchHandle.mergedResults} so a single combined grid can render
|
|
@@ -300,6 +360,15 @@ export interface SearchHandle {
|
|
|
300
360
|
globalPagination: GlobalPagination;
|
|
301
361
|
loading: boolean;
|
|
302
362
|
error: string | null;
|
|
363
|
+
/**
|
|
364
|
+
* CMS content types discovered at runtime from the CMS channel — each with
|
|
365
|
+
* its FQN and server-provided display `label` (empty until the first CMS
|
|
366
|
+
* result yields a channel id and discovery completes). The scope dropdown
|
|
367
|
+
* appends a `"<cmsKey>:<fqn>"` entry per type so a user can narrow a CMS
|
|
368
|
+
* search to a single content type. Empty for SObject-only configs or before
|
|
369
|
+
* discovery.
|
|
370
|
+
*/
|
|
371
|
+
discoveredContentTypes: DiscoveredContentType[];
|
|
303
372
|
resetAll: () => void;
|
|
304
373
|
}
|
|
305
374
|
|
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useNavigate } from "react-router";
|
|
3
|
-
import { Search } from "lucide-react";
|
|
4
|
-
import { Input } from "../components/ui/input";
|
|
5
|
-
import { Button } from "../components/ui/button";
|
|
1
|
+
import { GlobalSearchBox } from "../features/search";
|
|
6
2
|
|
|
7
3
|
export default function HomePage() {
|
|
8
|
-
const navigate = useNavigate();
|
|
9
|
-
const [text, setText] = useState("");
|
|
10
|
-
|
|
11
|
-
const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
|
|
12
|
-
e.preventDefault();
|
|
13
|
-
// `q` matches the search feature's GLOBAL_QUERY_KEY, so the term
|
|
14
|
-
// pre-fills the Account search page on arrival.
|
|
15
|
-
const params = text ? `?q=${encodeURIComponent(text)}` : "";
|
|
16
|
-
navigate(`/accounts${params}`);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
4
|
return (
|
|
20
5
|
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
<Button variant="outline" size="sm" onClick={() => navigate("/accounts")}>
|
|
24
|
-
Browse All Accounts
|
|
25
|
-
</Button>
|
|
26
|
-
</div>
|
|
27
|
-
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
28
|
-
<div className="relative flex-1">
|
|
29
|
-
<label htmlFor="account-search-input" className="sr-only">
|
|
30
|
-
Search accounts
|
|
31
|
-
</label>
|
|
32
|
-
<Search
|
|
33
|
-
aria-hidden="true"
|
|
34
|
-
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
|
35
|
-
/>
|
|
36
|
-
<Input
|
|
37
|
-
id="account-search-input"
|
|
38
|
-
type="text"
|
|
39
|
-
value={text}
|
|
40
|
-
onChange={(e) => setText(e.target.value)}
|
|
41
|
-
placeholder="Search by name, phone, or industry..."
|
|
42
|
-
className="pl-9"
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
<Button type="submit">Search</Button>
|
|
46
|
-
</form>
|
|
6
|
+
<h1 className="text-2xl font-bold mb-6">Home</h1>
|
|
7
|
+
<GlobalSearchBox placeholder="Search accounts, contacts, opportunities..." />
|
|
47
8
|
</div>
|
|
48
9
|
);
|
|
49
10
|
}
|
|
@@ -2,7 +2,7 @@ import type { RouteObject } from 'react-router';
|
|
|
2
2
|
import AppLayout from './appLayout';
|
|
3
3
|
import Home from './pages/Home';
|
|
4
4
|
import NotFound from './pages/NotFound';
|
|
5
|
-
import
|
|
5
|
+
import { Search as GlobalSearch, config } from "./features/search";
|
|
6
6
|
import AccountObjectDetail from "./pages/AccountObjectDetailPage";
|
|
7
7
|
|
|
8
8
|
export const routes: RouteObject[] = [
|
|
@@ -16,12 +16,19 @@ export const routes: RouteObject[] = [
|
|
|
16
16
|
handle: { showInNavigation: true, label: "Home" }
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
path: "
|
|
20
|
-
element:
|
|
19
|
+
path: "search",
|
|
20
|
+
element: (
|
|
21
|
+
<GlobalSearch
|
|
22
|
+
config={config}
|
|
23
|
+
title="Search"
|
|
24
|
+
searchPlaceholder="Search accounts, contacts, opportunities, and content..."
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
handle: { showInNavigation: true, label: "Search" }
|
|
21
28
|
},
|
|
22
29
|
{
|
|
23
|
-
path: "accounts",
|
|
24
|
-
element: <
|
|
30
|
+
path: "accounts/:recordId",
|
|
31
|
+
element: <AccountObjectDetail />
|
|
25
32
|
},
|
|
26
33
|
{
|
|
27
34
|
path: '*',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/app.tsx","./src/appLayout.tsx","./src/index.ts","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/api/graphqlClient.ts","./src/components/AgentforceConversationClient.tsx","./src/components/ObjectBreadcrumb.tsx","./src/components/__inherit_AgentforceConversationClient.tsx","./src/components/alerts/status-alert.tsx","./src/components/layouts/card-layout.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/datePicker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/features/search/index.ts","./src/features/search/loadConfig.ts","./src/features/search/queryBuilder.ts","./src/features/search/types.ts","./src/features/search/api/distinctValuesService.ts","./src/features/search/api/searchService.ts","./src/features/search/components/MergedSearchResults.tsx","./src/features/search/components/Search.tsx","./src/features/search/components/SearchResults.tsx","./src/features/search/components/SourceSection.tsx","./src/features/search/components/controls/PaginationControls.tsx","./src/features/search/components/controls/ScopeSelector.tsx","./src/features/search/components/controls/SearchBar.tsx","./src/features/search/components/controls/SortControl.tsx","./src/features/search/components/filters/ActiveFilters.tsx","./src/features/search/components/filters/DefaultFilterPanel.tsx","./src/features/search/components/filters/FilterContext.tsx","./src/features/search/components/filters/inputs/BooleanFilter.tsx","./src/features/search/components/filters/inputs/DateRangeFilter.tsx","./src/features/search/components/filters/inputs/FilterFieldWrapper.tsx","./src/features/search/components/filters/inputs/MultiSelectFilter.tsx","./src/features/search/components/filters/inputs/NumericRangeFilter.tsx","./src/features/search/components/filters/inputs/SelectFilter.tsx","./src/features/search/components/filters/inputs/TextFilter.tsx","./src/features/search/components/results/DefaultResultRow.tsx","./src/features/search/hooks/useAsyncData.ts","./src/features/search/hooks/useDistinctValues.ts","./src/features/search/hooks/useSearch.ts","./src/features/search/utils/debounce.ts","./src/features/search/utils/fieldUtils.ts","./src/features/search/utils/filterUtils.ts","./src/features/search/utils/sortUtils.ts","./src/hooks/useAsyncData.ts","./src/lib/utils.ts","./src/pages/AccountObjectDetailPage.tsx","./src/pages/
|
|
1
|
+
{"root":["./src/app.tsx","./src/appLayout.tsx","./src/index.ts","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/api/graphqlClient.ts","./src/components/AgentforceConversationClient.tsx","./src/components/ObjectBreadcrumb.tsx","./src/components/__inherit_AgentforceConversationClient.tsx","./src/components/alerts/status-alert.tsx","./src/components/layouts/card-layout.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/datePicker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/features/search/constants.ts","./src/features/search/index.ts","./src/features/search/loadConfig.ts","./src/features/search/queryBuilder.ts","./src/features/search/types.ts","./src/features/search/__tests__/queryBuilder.test.ts","./src/features/search/adapters/registry.ts","./src/features/search/adapters/types.ts","./src/features/search/adapters/cms/channelResolver.ts","./src/features/search/adapters/cms/cmsQueryFragment.ts","./src/features/search/adapters/cms/contentTypeSessionCache.ts","./src/features/search/adapters/cms/contentTypeUtils.ts","./src/features/search/adapters/cms/index.ts","./src/features/search/adapters/cms/parseResponse.ts","./src/features/search/adapters/cms/searchChannel.ts","./src/features/search/adapters/cms/types.ts","./src/features/search/adapters/cms/__tests__/channelResolver.test.ts","./src/features/search/adapters/cms/__tests__/cmsQueryFragment.test.ts","./src/features/search/adapters/cms/__tests__/contentTypeSessionCache.test.ts","./src/features/search/adapters/cms/__tests__/contentTypeUtils.test.ts","./src/features/search/adapters/cms/__tests__/orgApiVersionService.test.ts","./src/features/search/adapters/cms/__tests__/parseResponse.test.ts","./src/features/search/adapters/cms/__tests__/searchChannel.test.ts","./src/features/search/adapters/cms/__tests__/searchableContentTypesService.test.ts","./src/features/search/adapters/cms/api/apiUtils.ts","./src/features/search/adapters/cms/api/orgApiVersionService.ts","./src/features/search/adapters/cms/api/searchableContentTypesService.ts","./src/features/search/adapters/cms/hooks/useSearchableContentTypes.ts","./src/features/search/adapters/sobject/index.ts","./src/features/search/adapters/sobject/parseResponse.ts","./src/features/search/adapters/sobject/queryFragment.ts","./src/features/search/api/distinctValuesService.ts","./src/features/search/api/searchService.ts","./src/features/search/api/__tests__/searchService.test.ts","./src/features/search/components/GlobalSearchBox.tsx","./src/features/search/components/MergedSearchResults.tsx","./src/features/search/components/Search.tsx","./src/features/search/components/SearchResults.tsx","./src/features/search/components/SourceSection.tsx","./src/features/search/components/__tests__/Search.test.tsx","./src/features/search/components/controls/PaginationControls.tsx","./src/features/search/components/controls/ScopeSelector.tsx","./src/features/search/components/controls/SearchBar.tsx","./src/features/search/components/controls/SortControl.tsx","./src/features/search/components/filters/ActiveFilters.tsx","./src/features/search/components/filters/DefaultFilterPanel.tsx","./src/features/search/components/filters/FilterContext.tsx","./src/features/search/components/filters/inputs/BooleanFilter.tsx","./src/features/search/components/filters/inputs/DateRangeFilter.tsx","./src/features/search/components/filters/inputs/FilterFieldWrapper.tsx","./src/features/search/components/filters/inputs/MultiSelectFilter.tsx","./src/features/search/components/filters/inputs/NumericRangeFilter.tsx","./src/features/search/components/filters/inputs/SelectFilter.tsx","./src/features/search/components/filters/inputs/TextFilter.tsx","./src/features/search/components/results/CmsResultRow.tsx","./src/features/search/components/results/DefaultResultRow.tsx","./src/features/search/components/results/resolveResultRenderer.ts","./src/features/search/components/results/__tests__/CmsResultRow.test.tsx","./src/features/search/components/results/__tests__/resolveResultRenderer.test.ts","./src/features/search/hooks/useAsyncData.ts","./src/features/search/hooks/useDistinctValues.ts","./src/features/search/hooks/useSearch.ts","./src/features/search/utils/debounce.ts","./src/features/search/utils/fieldUtils.ts","./src/features/search/utils/filterUtils.ts","./src/features/search/utils/sortUtils.ts","./src/hooks/useAsyncData.ts","./src/lib/utils.ts","./src/pages/AccountObjectDetailPage.tsx","./src/pages/Home.tsx","./src/pages/NotFound.tsx","./src/types/conversation.ts","./src/types/globals.d.ts","./src/utils/accountFields.ts","./e2e/app.spec.ts","./vite-env.d.ts","./vitest-env.d.ts","./vitest.setup.ts","../../../../../../../../../sdk/platform-sdk/types/env.d.ts"],"version":"5.9.3"}
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.56.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
9
|
-
"version": "11.
|
|
9
|
+
"version": "11.56.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"fast-xml-parser": "^5.9.3",
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-app-react-template-b2e",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.56.0",
|
|
4
4
|
"description": "Salesforce React internal app template",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"!**/__tests__",
|
|
12
|
+
"!**/*.test.*"
|
|
11
13
|
],
|
|
12
14
|
"publishConfig": {
|
|
13
15
|
"access": "public"
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Search, config } from "../features/search";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Account search page.
|
|
5
|
-
*
|
|
6
|
-
* Renders the config-driven `<Search>` feature (feature-react-search) locked to
|
|
7
|
-
* the Account source via `restrictTo`. The shared `config.json` already defines
|
|
8
|
-
* the Account source's searchable fields, filters, sort options, and
|
|
9
|
-
* `routePattern` ("/accounts/:id"), so result rows link to the Account detail
|
|
10
|
-
* page and the filter / sort chrome render automatically — no per-page wiring.
|
|
11
|
-
*
|
|
12
|
-
* `restrictTo` hides the scope selector and fetches only Accounts, so the page
|
|
13
|
-
* reads as a single-object search even though the underlying feature supports
|
|
14
|
-
* many sources.
|
|
15
|
-
*/
|
|
16
|
-
export default function AccountSearch() {
|
|
17
|
-
return (
|
|
18
|
-
<Search
|
|
19
|
-
config={config}
|
|
20
|
-
title="Search Accounts"
|
|
21
|
-
searchPlaceholder="Search by name, phone, or industry..."
|
|
22
|
-
restrictTo={{ kind: "sobject", key: "accounts" }}
|
|
23
|
-
/>
|
|
24
|
-
);
|
|
25
|
-
}
|