@salesforce/webapp-template-feature-react-global-search-experimental 1.107.2 → 1.107.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/dist/.a4drules/skills/creating-webapp/SKILL.md +20 -0
- package/dist/.a4drules/skills/deploying-to-salesforce/SKILL.md +229 -0
- package/dist/.a4drules/skills/exploring-graphql-schema/SKILL.md +7 -18
- package/dist/.a4drules/skills/using-graphql/SKILL.md +2 -1
- package/dist/.a4drules/webapp-code-quality.md +5 -2
- package/dist/.a4drules/webapp-data-access.md +25 -0
- package/dist/.a4drules/webapp-deployment.md +32 -0
- package/dist/.a4drules/webapp-react-typescript.md +4 -12
- package/dist/.a4drules/webapp-react.md +7 -13
- package/dist/AGENT.md +3 -0
- package/dist/CHANGELOG.md +11 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/eslint.config.js +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/package.json +3 -9
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/app.tsx +4 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/alerts/status-alert.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +29 -27
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +3 -3
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +3 -3
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +16 -10
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/types/filters/filters.ts +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/utils/debounce.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.json +7 -1
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +7 -7
- package/package.json +1 -1
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +2 -2
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +2 -2
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +1 -1
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +29 -27
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/form.tsx +1 -1
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +3 -3
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +3 -3
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -1
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +1 -1
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +16 -10
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/types/filters/filters.ts +2 -2
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/utils/debounce.ts +1 -0
- package/src/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +1 -0
- package/dist/.a4drules/skills/generating-micro-frontend-lwc/SKILL.md +0 -137
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.tsbuildinfo +0 -1
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-base-sfdx-project-experimental",
|
|
3
|
-
"version": "1.107.
|
|
3
|
+
"version": "1.107.3",
|
|
4
4
|
"description": "Base SFDX project template",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"publishConfig": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"sf-project-setup": "node scripts/sf-project-setup.mjs",
|
|
11
11
|
"build": "echo 'No build required for base-sfdx-project'",
|
|
12
12
|
"clean": "echo 'No clean required for base-sfdx-project'",
|
|
13
|
-
"lint": "eslint **/{aura,lwc}/**/*.js",
|
|
13
|
+
"lint": "eslint --no-error-on-unmatched-pattern **/{aura,lwc}/**/*.js",
|
|
14
14
|
"test": "npm run test:unit",
|
|
15
15
|
"test:coverage": "npm run test",
|
|
16
16
|
"test:unit": "sfdx-lwc-jest -- --passWithNoTests",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"setup": "node scripts/setup-cli.mjs"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@lwc/eslint-plugin-lwc": "^
|
|
26
|
+
"@lwc/eslint-plugin-lwc": "^3.3.0",
|
|
27
27
|
"@prettier/plugin-xml": "^3.2.2",
|
|
28
|
-
"@salesforce/eslint-config-lwc": "^
|
|
29
|
-
"@salesforce/eslint-plugin-aura": "^
|
|
30
|
-
"@salesforce/eslint-plugin-lightning": "^
|
|
28
|
+
"@salesforce/eslint-config-lwc": "^4.1.0",
|
|
29
|
+
"@salesforce/eslint-plugin-aura": "^3.0.0",
|
|
30
|
+
"@salesforce/eslint-plugin-lightning": "^2.0.0",
|
|
31
31
|
"@salesforce/sfdx-lwc-jest": "^7.0.1",
|
|
32
|
-
"eslint": "
|
|
32
|
+
"eslint": "^9.39.0",
|
|
33
33
|
"eslint-plugin-import": "^2.25.4",
|
|
34
34
|
"eslint-plugin-jest": "^28.8.1",
|
|
35
35
|
"husky": "^9.1.5",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-feature-react-global-search-experimental",
|
|
3
|
-
"version": "1.107.
|
|
3
|
+
"version": "1.107.3",
|
|
4
4
|
"description": "Global search feature for Salesforce objects with filtering and pagination",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -104,10 +104,10 @@ function serializeSelectionTree(tree: SelectionTree, indent: string): string {
|
|
|
104
104
|
if (key === "Id") {
|
|
105
105
|
lines.push(`${indent}Id`);
|
|
106
106
|
} else {
|
|
107
|
-
lines.push(`${indent}${key} { value }`);
|
|
107
|
+
lines.push(`${indent}${key} @optional { value }`);
|
|
108
108
|
}
|
|
109
109
|
} else {
|
|
110
|
-
lines.push(`${indent}${key} {`);
|
|
110
|
+
lines.push(`${indent}${key} @optional {`);
|
|
111
111
|
lines.push(serializeSelectionTree(val, childIndent));
|
|
112
112
|
lines.push(`${indent}}`);
|
|
113
113
|
}
|
|
@@ -36,6 +36,8 @@ function FieldCell({
|
|
|
36
36
|
record: GraphQLRecordNode;
|
|
37
37
|
metadata?: ObjectInfoMetadata | null;
|
|
38
38
|
}) {
|
|
39
|
+
const labelId = useId();
|
|
40
|
+
const valueId = useId();
|
|
39
41
|
if (!item.isField || item.apiName == null) return null;
|
|
40
42
|
const label = item.label ?? item.apiName;
|
|
41
43
|
const hasComponents = item.layoutComponentApiNames && item.layoutComponentApiNames.length > 0;
|
|
@@ -51,8 +53,6 @@ function FieldCell({
|
|
|
51
53
|
: getDisplayValueForDetailFieldFromNode(record, item.apiName, metadata);
|
|
52
54
|
const dataType =
|
|
53
55
|
(hasComponents ? layoutResult?.dataType : undefined) ?? item.dataType ?? undefined;
|
|
54
|
-
const labelId = useId();
|
|
55
|
-
const valueId = useId();
|
|
56
56
|
return (
|
|
57
57
|
<div
|
|
58
58
|
className="flex flex-col gap-1"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* />
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
|
+
import React from "react";
|
|
26
27
|
import { useNavigate } from "react-router";
|
|
27
28
|
import { useMemo, useCallback } from "react";
|
|
28
29
|
import {
|
|
@@ -49,26 +50,24 @@ export default function SearchResultCard({
|
|
|
49
50
|
}: SearchResultCardProps) {
|
|
50
51
|
const navigate = useNavigate();
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!record.fields || typeof record.fields !== "object") {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
53
|
+
const validColumns = useMemo(
|
|
54
|
+
() => (columns && Array.isArray(columns) && columns.length > 0 ? columns : []),
|
|
55
|
+
[columns],
|
|
56
|
+
);
|
|
57
|
+
const validRecord =
|
|
58
|
+
record?.id && record?.fields && typeof record.fields === "object" ? record : null;
|
|
63
59
|
|
|
64
60
|
const detailPath = useMemo(
|
|
65
|
-
() =>
|
|
66
|
-
|
|
61
|
+
() =>
|
|
62
|
+
validRecord
|
|
63
|
+
? `/object/${objectApiName?.trim() || OBJECT_API_NAMES[0]}/${validRecord.id}`
|
|
64
|
+
: "",
|
|
65
|
+
[validRecord, objectApiName],
|
|
67
66
|
);
|
|
68
67
|
|
|
69
68
|
const handleClick = useCallback(() => {
|
|
70
|
-
if (
|
|
71
|
-
}, [
|
|
69
|
+
if (validRecord?.id) navigate(detailPath);
|
|
70
|
+
}, [validRecord?.id, detailPath, navigate]);
|
|
72
71
|
|
|
73
72
|
const handleKeyDown = useCallback(
|
|
74
73
|
(e: React.KeyboardEvent) => {
|
|
@@ -82,29 +81,32 @@ export default function SearchResultCard({
|
|
|
82
81
|
|
|
83
82
|
const primaryField = useMemo(() => {
|
|
84
83
|
return (
|
|
85
|
-
|
|
84
|
+
validColumns.find(
|
|
86
85
|
(col) =>
|
|
87
86
|
col &&
|
|
88
87
|
col.fieldApiName &&
|
|
89
88
|
(col.fieldApiName.toLowerCase() === "name" ||
|
|
90
89
|
col.fieldApiName.toLowerCase().includes("name")),
|
|
91
90
|
) ||
|
|
92
|
-
|
|
91
|
+
validColumns[0] ||
|
|
93
92
|
null
|
|
94
93
|
);
|
|
95
|
-
}, [
|
|
94
|
+
}, [validColumns]);
|
|
96
95
|
|
|
97
96
|
const primaryValue = useMemo(() => {
|
|
98
|
-
return primaryField && primaryField.fieldApiName
|
|
99
|
-
? getNestedFieldValue(
|
|
97
|
+
return primaryField && primaryField.fieldApiName && validRecord?.fields
|
|
98
|
+
? getNestedFieldValue(validRecord.fields, primaryField.fieldApiName) || "Untitled"
|
|
100
99
|
: "Untitled";
|
|
101
|
-
}, [primaryField,
|
|
100
|
+
}, [primaryField, validRecord]);
|
|
102
101
|
|
|
103
102
|
const secondaryColumns = useMemo(() => {
|
|
104
|
-
return
|
|
103
|
+
return validColumns.filter(
|
|
105
104
|
(col) => col && col.fieldApiName && col.fieldApiName !== primaryField?.fieldApiName,
|
|
106
105
|
);
|
|
107
|
-
}, [
|
|
106
|
+
}, [validColumns, primaryField]);
|
|
107
|
+
|
|
108
|
+
if (!validRecord) return null;
|
|
109
|
+
if (validColumns.length === 0) return null;
|
|
108
110
|
|
|
109
111
|
return (
|
|
110
112
|
<Card
|
|
@@ -114,19 +116,19 @@ export default function SearchResultCard({
|
|
|
114
116
|
role="button"
|
|
115
117
|
tabIndex={0}
|
|
116
118
|
aria-label={`View details for ${primaryValue}`}
|
|
117
|
-
aria-describedby={`result-${
|
|
119
|
+
aria-describedby={`result-${validRecord.id}-description`}
|
|
118
120
|
>
|
|
119
121
|
<CardHeader>
|
|
120
|
-
<CardTitle className="text-lg" id={`result-${
|
|
122
|
+
<CardTitle className="text-lg" id={`result-${validRecord.id}-title`}>
|
|
121
123
|
{primaryValue}
|
|
122
124
|
</CardTitle>
|
|
123
125
|
</CardHeader>
|
|
124
126
|
<CardContent>
|
|
125
|
-
<div id={`result-${
|
|
127
|
+
<div id={`result-${validRecord.id}-description`} className="sr-only">
|
|
126
128
|
Search result: {primaryValue}
|
|
127
129
|
</div>
|
|
128
130
|
<ResultCardFields
|
|
129
|
-
record={
|
|
131
|
+
record={validRecord}
|
|
130
132
|
columns={secondaryColumns}
|
|
131
133
|
excludeFieldApiName={primaryField?.fieldApiName}
|
|
132
134
|
/>
|
|
@@ -35,10 +35,10 @@ export function useObjectInfoBatch(objectApiNames: string[]): UseObjectInfoBatch
|
|
|
35
35
|
isCancelled.current = false;
|
|
36
36
|
const names = objectApiNames.filter(Boolean);
|
|
37
37
|
if (names.length === 0) {
|
|
38
|
-
setState({ objectInfos: [], loading: false, error: null });
|
|
38
|
+
queueMicrotask(() => setState({ objectInfos: [], loading: false, error: null }));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
setState((s) => ({ ...s, loading: true, error: null }));
|
|
41
|
+
queueMicrotask(() => setState((s) => ({ ...s, loading: true, error: null })));
|
|
42
42
|
objectInfoService
|
|
43
43
|
.getObjectInfoBatch(names.join(","))
|
|
44
44
|
.then((res) => {
|
|
@@ -59,7 +59,7 @@ export function useObjectInfoBatch(objectApiNames: string[]): UseObjectInfoBatch
|
|
|
59
59
|
return () => {
|
|
60
60
|
isCancelled.current = true;
|
|
61
61
|
};
|
|
62
|
-
}, [objectApiNames
|
|
62
|
+
}, [objectApiNames]);
|
|
63
63
|
|
|
64
64
|
return state;
|
|
65
65
|
}
|
|
@@ -85,14 +85,14 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
85
85
|
|
|
86
86
|
useEffect(() => {
|
|
87
87
|
if (!objectApiName) {
|
|
88
|
-
setState((s) => ({ ...s, loading: false, error: "Invalid object" }));
|
|
88
|
+
queueMicrotask(() => setState((s) => ({ ...s, loading: false, error: "Invalid object" })));
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
let isCancelled = false;
|
|
93
93
|
|
|
94
94
|
const run = async () => {
|
|
95
|
-
setState((s) => ({ ...s, loading: true, error: null }));
|
|
95
|
+
queueMicrotask(() => setState((s) => ({ ...s, loading: true, error: null })));
|
|
96
96
|
try {
|
|
97
97
|
const filters = await getSharedFilters(objectApiName!);
|
|
98
98
|
if (isCancelled) return;
|
|
@@ -119,7 +119,7 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
119
119
|
loading: false,
|
|
120
120
|
error: null,
|
|
121
121
|
});
|
|
122
|
-
} catch
|
|
122
|
+
} catch {
|
|
123
123
|
if (isCancelled) return;
|
|
124
124
|
setState((s) => ({
|
|
125
125
|
...s,
|
|
@@ -110,7 +110,7 @@ export function useRecordDetailLayout({
|
|
|
110
110
|
setLayout(layoutData);
|
|
111
111
|
setRecord(recordData);
|
|
112
112
|
setObjectMetadata(objectMetadataData);
|
|
113
|
-
} catch
|
|
113
|
+
} catch {
|
|
114
114
|
if (isCancelled) return;
|
|
115
115
|
setError("Failed to load record details");
|
|
116
116
|
} finally {
|
|
@@ -115,7 +115,7 @@ export function useRecordListGraphQL(
|
|
|
115
115
|
useEffect(() => {
|
|
116
116
|
if (!objectApiName || columnsLoading || columnsError) return;
|
|
117
117
|
if (columns.length === 0 && !columnsLoading) return;
|
|
118
|
-
fetchRecords();
|
|
118
|
+
queueMicrotask(() => fetchRecords());
|
|
119
119
|
}, [objectApiName, columns, columnsLoading, columnsError, fetchRecords]);
|
|
120
120
|
|
|
121
121
|
const objectData = data?.uiapi?.query?.[objectApiName];
|
|
@@ -46,7 +46,7 @@ export default function GlobalSearch() {
|
|
|
46
46
|
if (!query) return "";
|
|
47
47
|
try {
|
|
48
48
|
return decodeURIComponent(query);
|
|
49
|
-
} catch
|
|
49
|
+
} catch {
|
|
50
50
|
return query;
|
|
51
51
|
}
|
|
52
52
|
}, [query]);
|
|
@@ -56,9 +56,11 @@ export default function GlobalSearch() {
|
|
|
56
56
|
|
|
57
57
|
// Reset pagination when the URL search query changes so we don't use an old cursor with a new result set
|
|
58
58
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
setAfterCursor(null);
|
|
61
|
+
setPageIndex(0);
|
|
62
|
+
setCursorStack([null]);
|
|
63
|
+
});
|
|
62
64
|
}, [query]);
|
|
63
65
|
|
|
64
66
|
const listMeta = useObjectListMetadata(objectApiName);
|
|
@@ -87,10 +89,12 @@ export default function GlobalSearch() {
|
|
|
87
89
|
if (resultsLoading) return;
|
|
88
90
|
const cursor = pageInfo?.endCursor ?? null;
|
|
89
91
|
if (cursor == null) return;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
setCursorStack((prev) => {
|
|
94
|
+
const next = [...prev];
|
|
95
|
+
next[pageIndex + 1] = cursor;
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
94
98
|
});
|
|
95
99
|
}, [resultsLoading, pageInfo?.endCursor, pageIndex]);
|
|
96
100
|
|
|
@@ -116,8 +120,10 @@ export default function GlobalSearch() {
|
|
|
116
120
|
|
|
117
121
|
const cursorStackRef = useRef(cursorStack);
|
|
118
122
|
const pageIndexRef = useRef(pageIndex);
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
cursorStackRef.current = cursorStack;
|
|
125
|
+
pageIndexRef.current = pageIndex;
|
|
126
|
+
}, [cursorStack, pageIndex]);
|
|
121
127
|
|
|
122
128
|
const canRenderFilters =
|
|
123
129
|
!listMeta.loading && listMeta.filters !== undefined && listMeta.picklistValues !== undefined;
|
|
@@ -108,8 +108,8 @@ export type FilterCriteria = z.infer<typeof FilterCriteriaSchema>;
|
|
|
108
108
|
// Export schema for validation
|
|
109
109
|
export const FilterCriteriaArraySchema = z.array(FilterCriteriaSchema);
|
|
110
110
|
|
|
111
|
-
// Zod Schema for Filters Response
|
|
112
|
-
const FiltersResponseSchema = z.record(z.string(), z.unknown()).and(
|
|
111
|
+
// Zod Schema for Filters Response (used for type inference via z.infer)
|
|
112
|
+
export const FiltersResponseSchema = z.record(z.string(), z.unknown()).and(
|
|
113
113
|
z.object({
|
|
114
114
|
filters: FilterArraySchema.optional(),
|
|
115
115
|
}),
|
|
@@ -57,6 +57,7 @@ export function debounce<T extends (...args: any[]) => any>(
|
|
|
57
57
|
let lastArgs: Parameters<T> | null = null;
|
|
58
58
|
function debounced(this: ThisParameterType<T>, ...args: Parameters<T>) {
|
|
59
59
|
// 2. Context Safety: Capture 'this' to support class methods
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- required for debouncing class methods
|
|
60
61
|
lastContext = this;
|
|
61
62
|
lastArgs = args;
|
|
62
63
|
if (timeoutId) {
|
|
@@ -43,6 +43,7 @@ export function sanitizeFilterValue(value: string, maxLength: number = 1000): st
|
|
|
43
43
|
sanitized = sanitized.substring(0, maxLength);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// eslint-disable-next-line no-control-regex -- intentionally matching control chars for sanitization
|
|
46
47
|
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
47
48
|
|
|
48
49
|
return sanitized;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: generating-micro-frontend-lwc
|
|
3
|
-
description: Generate a Micro Frontend LWC component for a Web Application.
|
|
4
|
-
license: Proprietary. LICENSE.txt has complete terms
|
|
5
|
-
metadata:
|
|
6
|
-
author: salesforce-experience-platform-emu/lwc-admins
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Micro Frontend generation (workflow)
|
|
10
|
-
|
|
11
|
-
When the user wants a Micro Frontend for a Web Application, follow this workflow.
|
|
12
|
-
|
|
13
|
-
## 1. Install the dependency
|
|
14
|
-
|
|
15
|
-
Micro Frontends are generated using the `generate-micro-frontend` CLI command from the `@salesforce/micro-frontends-experimental` package.
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @salesforce/micro-frontends-experimental
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The dependency should be added to the project's `package.json` dependencies.
|
|
22
|
-
|
|
23
|
-
## 2. Identify the Web Application
|
|
24
|
-
|
|
25
|
-
- Verify the Web Application exists in `force-app/main/default/webapplications/<web-app-name>/`.
|
|
26
|
-
- Confirm the Web Application has a `lightningOut` target in its `webapplication-meta.xml` file.
|
|
27
|
-
- If no Web Application exists, the user must create one first using the Web Apps template system.
|
|
28
|
-
|
|
29
|
-
## 3. Generate the Micro Frontend component
|
|
30
|
-
|
|
31
|
-
Run the `generate-micro-frontend` command with the Web Application name from the root of an SFDX project:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npx generate-micro-frontend <web-app-name>
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
This creates:
|
|
38
|
-
|
|
39
|
-
- A custom wrapper LWC component in `force-app/main/default/lwc/<webAppName>/`. This is the "Micro Frontend component".
|
|
40
|
-
- The static `microFrontendShell` component that handles iframe communication.
|
|
41
|
-
|
|
42
|
-
Notes:
|
|
43
|
-
|
|
44
|
-
- The command may be added to the project's `package.json` scripts for convenience.
|
|
45
|
-
- The Micro Frontend component uses the Web Application name (e.g. `my-web-app/`) in camelCase for its folder and file names (e.g. `myWebApp/myWebApp.js`, `myWebApp/myWebApp.html`).
|
|
46
|
-
|
|
47
|
-
## 4. Customize the Micro Frontend component metadata
|
|
48
|
-
|
|
49
|
-
Edit the `<webAppName>.js-meta.xml` file to:
|
|
50
|
-
|
|
51
|
-
- Set appropriate `targets` (e.g. `lightning__HomePage`, `lightning__AppPage`, `lightning__RecordPage`, `lightningCommunity__Page`)
|
|
52
|
-
- Add `targetConfigs` for page-specific properties
|
|
53
|
-
- Optionally update the `masterLabel` and `description`
|
|
54
|
-
|
|
55
|
-
Example:
|
|
56
|
-
|
|
57
|
-
```xml
|
|
58
|
-
<targetConfigs>
|
|
59
|
-
<targetConfig targets="lightning__HomePage">
|
|
60
|
-
<property name="height" type="Integer" min="0" max="600" default="300" />
|
|
61
|
-
</targetConfig>
|
|
62
|
-
</targetConfigs>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## 5. Pass properties to the Micro Frontend component
|
|
66
|
-
|
|
67
|
-
Edit the `<webAppName>.js` file to customize the `properties` getter:
|
|
68
|
-
|
|
69
|
-
```javascript
|
|
70
|
-
@api height;
|
|
71
|
-
|
|
72
|
-
@api get properties() {
|
|
73
|
-
return {
|
|
74
|
-
height: this.height,
|
|
75
|
-
// Add any other data your Web Application needs
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
All properties are passed to the embedded Web Application via `postMessage` and can be accessed in the app's code.
|
|
81
|
-
|
|
82
|
-
## 6. Deploy and test
|
|
83
|
-
|
|
84
|
-
Deploy the Micro Frontend component using standard SF CLI commands:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
sf project deploy start --source-dir force-app/main/default
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Add the component to a page using Lightning App Builder or Experience Builder and verify it loads correctly.
|
|
91
|
-
|
|
92
|
-
# Micro Frontend component customization examples
|
|
93
|
-
|
|
94
|
-
## Record page example
|
|
95
|
-
|
|
96
|
-
Command to generate a Micro Frontend component for the `my-site` Web Application:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npx generate-micro-frontend my-site
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`mySite.js-meta.xml` file with a `lightning__RecordPage` target:
|
|
103
|
-
|
|
104
|
-
```xml
|
|
105
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
106
|
-
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
107
|
-
<apiVersion>66.0</apiVersion>
|
|
108
|
-
<isExposed>true</isExposed>
|
|
109
|
-
<masterLabel>Micro Frontend for "My Site"</masterLabel>
|
|
110
|
-
<targets>
|
|
111
|
-
<target>lightning__RecordPage</target>
|
|
112
|
-
</targets>
|
|
113
|
-
<targetConfigs>
|
|
114
|
-
<targetConfig targets="lightning__RecordPage">
|
|
115
|
-
<property name="mode" type="String" default="dark" />
|
|
116
|
-
</targetConfig>
|
|
117
|
-
</targetConfigs>
|
|
118
|
-
</LightningComponentBundle>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
`mySite.js` file with public properties and `properties` getter:
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
// Micro Frontend component
|
|
125
|
-
export default class mySite extends LightningElement {
|
|
126
|
-
@api recordId;
|
|
127
|
-
@api mode;
|
|
128
|
-
|
|
129
|
-
@api get properties() {
|
|
130
|
-
// This data is passed to the Micro Frontend
|
|
131
|
-
return {
|
|
132
|
-
recordId: this.recordId, // automatically populated for lightning__RecordPage target
|
|
133
|
-
mode: this.mode, // matches the mode targetConfig
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
```
|
package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/app.tsx","./src/appLayout.tsx","./src/index.ts","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/components/alerts/status-alert.tsx","./src/components/layouts/card-layout.tsx","./src/components/ui/alert.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/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/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/features/global-search/constants.ts","./src/features/global-search/api/objectDetailService.ts","./src/features/global-search/api/objectInfoGraphQLService.ts","./src/features/global-search/api/objectInfoService.ts","./src/features/global-search/api/recordListGraphQLService.ts","./src/features/global-search/components/detail/DetailFields.tsx","./src/features/global-search/components/detail/DetailForm.tsx","./src/features/global-search/components/detail/DetailHeader.tsx","./src/features/global-search/components/detail/DetailLayoutSections.tsx","./src/features/global-search/components/detail/Section.tsx","./src/features/global-search/components/detail/SectionRow.tsx","./src/features/global-search/components/detail/UiApiDetailForm.tsx","./src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx","./src/features/global-search/components/detail/formatted/FormattedAddress.tsx","./src/features/global-search/components/detail/formatted/FormattedEmail.tsx","./src/features/global-search/components/detail/formatted/FormattedPhone.tsx","./src/features/global-search/components/detail/formatted/FormattedText.tsx","./src/features/global-search/components/detail/formatted/FormattedUrl.tsx","./src/features/global-search/components/filters/FilterField.tsx","./src/features/global-search/components/filters/FilterInput.tsx","./src/features/global-search/components/filters/FilterSelect.tsx","./src/features/global-search/components/filters/FiltersPanel.tsx","./src/features/global-search/components/forms/filters-form.tsx","./src/features/global-search/components/forms/submit-button.tsx","./src/features/global-search/components/search/GlobalSearchInput.tsx","./src/features/global-search/components/search/ResultCardFields.tsx","./src/features/global-search/components/search/SearchHeader.tsx","./src/features/global-search/components/search/SearchPagination.tsx","./src/features/global-search/components/search/SearchResultCard.tsx","./src/features/global-search/components/search/SearchResultsPanel.tsx","./src/features/global-search/components/shared/LoadingFallback.tsx","./src/features/global-search/filters/FilterInput.tsx","./src/features/global-search/filters/FilterSelect.tsx","./src/features/global-search/hooks/form.tsx","./src/features/global-search/hooks/useObjectInfoBatch.ts","./src/features/global-search/hooks/useObjectSearchData.ts","./src/features/global-search/hooks/useRecordDetailLayout.ts","./src/features/global-search/hooks/useRecordListGraphQL.ts","./src/features/global-search/pages/DetailPage.tsx","./src/features/global-search/pages/GlobalSearch.tsx","./src/features/global-search/types/schema.d.ts","./src/features/global-search/types/filters/filters.ts","./src/features/global-search/types/filters/picklist.ts","./src/features/global-search/types/objectInfo/objectInfo.ts","./src/features/global-search/types/recordDetail/recordDetail.ts","./src/features/global-search/types/search/searchResults.ts","./src/features/global-search/utils/apiUtils.ts","./src/features/global-search/utils/cacheUtils.ts","./src/features/global-search/utils/debounce.ts","./src/features/global-search/utils/fieldUtils.ts","./src/features/global-search/utils/fieldValueExtractor.ts","./src/features/global-search/utils/filterUtils.ts","./src/features/global-search/utils/formDataTransformUtils.ts","./src/features/global-search/utils/formUtils.ts","./src/features/global-search/utils/graphQLNodeFieldUtils.ts","./src/features/global-search/utils/graphQLObjectInfoAdapter.ts","./src/features/global-search/utils/graphQLRecordAdapter.ts","./src/features/global-search/utils/layoutTransformUtils.ts","./src/features/global-search/utils/linkUtils.ts","./src/features/global-search/utils/paginationUtils.ts","./src/features/global-search/utils/recordUtils.ts","./src/features/global-search/utils/sanitizationUtils.ts","./src/lib/utils.ts","./src/pages/Home.tsx","./src/pages/NotFound.tsx","./vite-env.d.ts","./vitest-env.d.ts"],"version":"5.9.3"}
|