@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.
Files changed (35) hide show
  1. package/dist/.a4drules/skills/creating-webapp/SKILL.md +20 -0
  2. package/dist/.a4drules/skills/deploying-to-salesforce/SKILL.md +229 -0
  3. package/dist/.a4drules/skills/exploring-graphql-schema/SKILL.md +7 -18
  4. package/dist/.a4drules/skills/using-graphql/SKILL.md +2 -1
  5. package/dist/.a4drules/webapp-code-quality.md +5 -2
  6. package/dist/.a4drules/webapp-data-access.md +25 -0
  7. package/dist/.a4drules/webapp-deployment.md +32 -0
  8. package/dist/.a4drules/webapp-react-typescript.md +4 -12
  9. package/dist/.a4drules/webapp-react.md +7 -13
  10. package/dist/AGENT.md +3 -0
  11. package/dist/CHANGELOG.md +11 -0
  12. package/dist/eslint.config.js +7 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/eslint.config.js +2 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +4 -9
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/app.tsx +4 -1
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/alerts/status-alert.tsx +1 -1
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/recordListGraphQLService.ts +2 -2
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailForm.tsx +2 -2
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FiltersPanel.tsx +1 -1
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultCard.tsx +29 -27
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/form.tsx +1 -1
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +3 -3
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectSearchData.ts +3 -3
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -1
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +1 -1
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/GlobalSearch.tsx +16 -10
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/filters.ts +2 -2
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/debounce.ts +1 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/sanitizationUtils.ts +1 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/tsconfig.json +7 -1
  31. package/dist/package-lock.json +9995 -0
  32. package/dist/package.json +7 -7
  33. package/package.json +1 -1
  34. package/dist/.a4drules/skills/generating-micro-frontend-lwc/SKILL.md +0 -137
  35. 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
- if (!record || !record.id) {
53
- return null;
54
- }
55
-
56
- if (!columns || !Array.isArray(columns) || columns.length === 0) {
57
- return null;
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
- () => `/object/${objectApiName?.trim() || OBJECT_API_NAMES[0]}/${record.id}`,
66
- [record.id, objectApiName],
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 (record.id) navigate(detailPath);
71
- }, [record.id, detailPath, navigate]);
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
- columns.find(
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
- columns[0] ||
91
+ validColumns[0] ||
93
92
  null
94
93
  );
95
- }, [columns]);
94
+ }, [validColumns]);
96
95
 
97
96
  const primaryValue = useMemo(() => {
98
- return primaryField && primaryField.fieldApiName
99
- ? getNestedFieldValue(record.fields, primaryField.fieldApiName) || "Untitled"
97
+ return primaryField && primaryField.fieldApiName && validRecord?.fields
98
+ ? getNestedFieldValue(validRecord.fields, primaryField.fieldApiName) || "Untitled"
100
99
  : "Untitled";
101
- }, [primaryField, record.fields]);
100
+ }, [primaryField, validRecord]);
102
101
 
103
102
  const secondaryColumns = useMemo(() => {
104
- return columns.filter(
103
+ return validColumns.filter(
105
104
  (col) => col && col.fieldApiName && col.fieldApiName !== primaryField?.fieldApiName,
106
105
  );
107
- }, [columns, primaryField]);
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-${record.id}-description`}
119
+ aria-describedby={`result-${validRecord.id}-description`}
118
120
  >
119
121
  <CardHeader>
120
- <CardTitle className="text-lg" id={`result-${record.id}-title`}>
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-${record.id}-description`} className="sr-only">
127
+ <div id={`result-${validRecord.id}-description`} className="sr-only">
126
128
  Search result: {primaryValue}
127
129
  </div>
128
130
  <ResultCardFields
129
- record={record}
131
+ record={validRecord}
130
132
  columns={secondaryColumns}
131
133
  excludeFieldApiName={primaryField?.fieldApiName}
132
134
  />
@@ -1,4 +1,4 @@
1
- import { useId } from "react";
1
+ import React, { useId } from "react";
2
2
  import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
3
3
  import {
4
4
  Field,
@@ -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.join(",")]);
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 (err) {
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 (err) {
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 (e) {
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
- setAfterCursor(null);
60
- setPageIndex(0);
61
- setCursorStack([null]);
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
- setCursorStack((prev) => {
91
- const next = [...prev];
92
- next[pageIndex + 1] = cursor;
93
- return next;
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
- cursorStackRef.current = cursorStack;
120
- pageIndexRef.current = pageIndex;
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": ["src", "vite-env.d.ts", "vitest-env.d.ts"],
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
  }