@salesforce/webapp-template-app-react-template-b2e-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/appreacttemplateb2e/eslint.config.js +2 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +4 -9
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/app.tsx +4 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/alerts/status-alert.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/recordListGraphQLService.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailForm.tsx +2 -2
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FiltersPanel.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultCard.tsx +29 -27
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +3 -3
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectSearchData.ts +3 -3
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/GlobalSearch.tsx +16 -10
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/filters.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/debounce.ts +1 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/sanitizationUtils.ts +1 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/tsconfig.json +7 -1
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +7 -7
- package/package.json +1 -1
- package/dist/.a4drules/skills/generating-micro-frontend-lwc/SKILL.md +0 -137
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/tsconfig.tsbuildinfo +0 -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;
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"@assets/*": ["./src/assets/*"]
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
"include": [
|
|
34
|
+
"include": [
|
|
35
|
+
"src",
|
|
36
|
+
"e2e",
|
|
37
|
+
"vite-env.d.ts",
|
|
38
|
+
"vitest-env.d.ts",
|
|
39
|
+
"vitest.setup.ts"
|
|
40
|
+
],
|
|
35
41
|
"references": [{ "path": "./tsconfig.node.json" }]
|
|
36
42
|
}
|