@salesforce/webapp-template-app-react-template-b2x-experimental 1.109.5 → 1.109.6

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 (102) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package.json +4 -3
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/FilterPanel.tsx +127 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/PaginationControls.tsx +151 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SortControl.tsx +143 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useAsyncData.ts +53 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/debounce.ts +22 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/Home.tsx +10 -11
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/routes.tsx +8 -20
  37. package/dist/package-lock.json +2 -2
  38. package/dist/package.json +1 -1
  39. package/package.json +1 -1
  40. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  41. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/constants.ts +0 -39
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/form.tsx +0 -209
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/filters.ts +0 -121
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/schema.d.ts +0 -200
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/search/searchResults.ts +0 -229
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/debounce.ts +0 -90
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formUtils.ts +0 -142
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/index.ts +0 -120
@@ -1,137 +0,0 @@
1
- import { useState, useEffect, useRef } from "react";
2
- import { objectDetailService } from "../api/objectDetailService";
3
- import type { LayoutResponse } from "../types/recordDetail/recordDetail";
4
- import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
5
- import type { GraphQLRecordNode } from "../api/recordListGraphQLService";
6
-
7
- export interface UseRecordDetailLayoutReturn {
8
- layout: LayoutResponse | null;
9
- record: GraphQLRecordNode | null;
10
- objectMetadata: ObjectInfoResult | null;
11
- loading: boolean;
12
- error: string | null;
13
- }
14
-
15
- export interface UseRecordDetailLayoutParams {
16
- objectApiName: string | null;
17
- recordId: string | null;
18
- recordTypeId?: string | null;
19
- initialData?: {
20
- layout: LayoutResponse;
21
- record: GraphQLRecordNode;
22
- objectMetadata: ObjectInfoResult;
23
- } | null;
24
- }
25
-
26
- const MAX_CACHE_SIZE = 50;
27
- const CACHE_TTL_MS = 5 * 60 * 1000;
28
-
29
- type CacheEntry = {
30
- layout: LayoutResponse;
31
- record: GraphQLRecordNode;
32
- objectMetadata: ObjectInfoResult;
33
- cachedAt: number;
34
- };
35
-
36
- export function useRecordDetailLayout({
37
- objectApiName,
38
- recordId,
39
- recordTypeId = null,
40
- initialData = null,
41
- }: UseRecordDetailLayoutParams): UseRecordDetailLayoutReturn {
42
- const [layout, setLayout] = useState<LayoutResponse | null>(initialData?.layout ?? null);
43
- const [record, setRecord] = useState<GraphQLRecordNode | null>(initialData?.record ?? null);
44
- const [objectMetadata, setObjectMetadata] = useState<ObjectInfoResult | null>(
45
- initialData?.objectMetadata ?? null,
46
- );
47
- const [loading, setLoading] = useState(!initialData);
48
- const [error, setError] = useState<string | null>(null);
49
-
50
- const cacheKey =
51
- objectApiName && recordId ? `${objectApiName}:${recordId}:${recordTypeId ?? "default"}` : null;
52
- const cacheRef = useRef<Map<string, CacheEntry>>(new Map());
53
-
54
- useEffect(() => {
55
- if (!objectApiName || !recordId) {
56
- setError("Invalid object or record ID");
57
- setLoading(false);
58
- return;
59
- }
60
-
61
- if (
62
- initialData?.layout != null &&
63
- initialData?.record != null &&
64
- initialData?.objectMetadata != null
65
- ) {
66
- return;
67
- }
68
-
69
- const cached = cacheRef.current.get(cacheKey!);
70
- const now = Date.now();
71
- if (cached && now - cached.cachedAt < CACHE_TTL_MS) {
72
- setLayout(cached.layout);
73
- setRecord(cached.record);
74
- setObjectMetadata(cached.objectMetadata);
75
- setLoading(false);
76
- setError(null);
77
- return;
78
- }
79
-
80
- let isCancelled = false;
81
-
82
- const fetchDetail = async () => {
83
- setLoading(true);
84
- setError(null);
85
-
86
- try {
87
- const {
88
- layout: layoutData,
89
- record: recordData,
90
- objectMetadata: objectMetadataData,
91
- } = await objectDetailService.getRecordDetail(
92
- objectApiName,
93
- recordId,
94
- recordTypeId ?? undefined,
95
- );
96
-
97
- if (isCancelled) return;
98
-
99
- const cache = cacheRef.current;
100
- if (cache.size >= MAX_CACHE_SIZE) {
101
- const firstKey = cache.keys().next().value;
102
- if (firstKey != null) cache.delete(firstKey);
103
- }
104
- cache.set(cacheKey!, {
105
- layout: layoutData,
106
- record: recordData,
107
- objectMetadata: objectMetadataData,
108
- cachedAt: Date.now(),
109
- });
110
- setLayout(layoutData);
111
- setRecord(recordData);
112
- setObjectMetadata(objectMetadataData);
113
- } catch {
114
- if (isCancelled) return;
115
- setError("Failed to load record details");
116
- } finally {
117
- if (!isCancelled) {
118
- setLoading(false);
119
- }
120
- }
121
- };
122
-
123
- fetchDetail();
124
-
125
- return () => {
126
- isCancelled = true;
127
- };
128
- }, [objectApiName, recordId, recordTypeId, cacheKey, initialData]);
129
-
130
- return {
131
- layout,
132
- record,
133
- objectMetadata,
134
- loading,
135
- error,
136
- };
137
- }
@@ -1,135 +0,0 @@
1
- /**
2
- * Record list hook: GraphQL records with filter, sort, pagination, search.
3
- * Use for list/search views; detail view uses useRecordDetailLayout instead.
4
- *
5
- * @module hooks/useRecordListGraphQL
6
- */
7
-
8
- import { useState, useEffect, useCallback } from "react";
9
- import { useObjectColumns } from "./useObjectSearchData";
10
- import {
11
- getRecordsGraphQL,
12
- buildOrderByFromSort,
13
- type RecordListGraphQLResult,
14
- } from "../api/recordListGraphQLService";
15
- import type { Column } from "../types/search/searchResults";
16
- import type { FilterCriteria } from "../types/filters/filters";
17
-
18
- const EMPTY_FILTERS: FilterCriteria[] = [];
19
-
20
- export interface UseRecordListGraphQLOptions {
21
- objectApiName: string;
22
- first?: number;
23
- after?: string | null;
24
- filters?: FilterCriteria[];
25
- sortBy?: string;
26
- searchQuery?: string;
27
- /** When provided, skips useObjectColumns (use from parent e.g. useObjectListMetadata). */
28
- columns?: Column[];
29
- columnsLoading?: boolean;
30
- columnsError?: string | null;
31
- }
32
-
33
- export interface UseRecordListGraphQLReturn {
34
- data: RecordListGraphQLResult | null;
35
- edges: Array<{ node?: Record<string, unknown> }>;
36
- pageInfo: {
37
- hasNextPage?: boolean;
38
- hasPreviousPage?: boolean;
39
- endCursor?: string | null;
40
- startCursor?: string | null;
41
- } | null;
42
- loading: boolean;
43
- error: string | null;
44
- columnsLoading: boolean;
45
- columnsError: string | null;
46
- refetch: () => void;
47
- }
48
-
49
- /**
50
- * Fetches records via GraphQL for the given object with filter, sort, pagination, and search.
51
- */
52
- export function useRecordListGraphQL(
53
- options: UseRecordListGraphQLOptions,
54
- ): UseRecordListGraphQLReturn {
55
- const {
56
- objectApiName,
57
- first = 50,
58
- after = null,
59
- filters = EMPTY_FILTERS,
60
- sortBy = "",
61
- searchQuery = "",
62
- columns: columnsProp,
63
- columnsLoading: columnsLoadingProp,
64
- columnsError: columnsErrorProp,
65
- } = options;
66
-
67
- const fromParent = columnsProp !== undefined;
68
- const fromHook = useObjectColumns(fromParent ? null : objectApiName);
69
-
70
- const columns = fromParent ? columnsProp : fromHook.columns;
71
- const columnsLoading = fromParent ? (columnsLoadingProp ?? false) : fromHook.columnsLoading;
72
- const columnsError = fromParent ? (columnsErrorProp ?? null) : fromHook.columnsError;
73
-
74
- const [data, setData] = useState<RecordListGraphQLResult | null>(null);
75
- const [loading, setLoading] = useState(false);
76
- const [error, setError] = useState<string | null>(null);
77
-
78
- const fetchRecords = useCallback(() => {
79
- if (columnsLoading || columnsError || columns.length === 0) return;
80
-
81
- setLoading(true);
82
- setError(null);
83
- const orderBy = buildOrderByFromSort(sortBy);
84
-
85
- getRecordsGraphQL({
86
- objectApiName,
87
- columns,
88
- first,
89
- after,
90
- filters,
91
- orderBy,
92
- searchQuery: searchQuery.trim() || undefined,
93
- })
94
- .then((result) => {
95
- setData(result);
96
- })
97
- .catch((err) => {
98
- setError(err instanceof Error ? err.message : "Failed to load records");
99
- })
100
- .finally(() => {
101
- setLoading(false);
102
- });
103
- }, [
104
- objectApiName,
105
- columns,
106
- columnsLoading,
107
- columnsError,
108
- first,
109
- after,
110
- filters,
111
- sortBy,
112
- searchQuery,
113
- ]);
114
-
115
- useEffect(() => {
116
- if (!objectApiName || columnsLoading || columnsError) return;
117
- if (columns.length === 0 && !columnsLoading) return;
118
- queueMicrotask(() => fetchRecords());
119
- }, [objectApiName, columns, columnsLoading, columnsError, fetchRecords]);
120
-
121
- const objectData = data?.uiapi?.query?.[objectApiName];
122
- const edges = objectData?.edges ?? [];
123
- const pageInfo = objectData?.pageInfo ?? null;
124
-
125
- return {
126
- data,
127
- edges,
128
- pageInfo,
129
- loading: columnsLoading || loading,
130
- error: columnsError || error,
131
- columnsLoading,
132
- columnsError,
133
- refetch: fetchRecords,
134
- };
135
- }
@@ -1,109 +0,0 @@
1
- import { useMemo } from "react";
2
- import { useParams, useNavigate } from "react-router";
3
- import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/card";
4
- import { Skeleton } from "../../../components/ui/skeleton";
5
- import { Alert, AlertDescription, AlertTitle } from "../../../components/ui/alert";
6
- import { AlertCircle } from "lucide-react";
7
- import DetailHeader from "../components/detail/DetailHeader";
8
- import { UiApiDetailForm } from "../components/detail/UiApiDetailForm";
9
- import { OBJECT_API_NAMES, DEFAULT_DETAIL_PAGE_TITLE } from "../constants";
10
- import { toRecordDisplayNameMetadata } from "../utils/fieldUtils";
11
- import { useRecordDetailLayout } from "../hooks/useRecordDetailLayout";
12
- import { getGraphQLRecordDisplayName } from "../utils/graphQLNodeFieldUtils";
13
-
14
- export default function DetailPage() {
15
- const { objectApiName: objectApiNameParam, recordId } = useParams<{
16
- objectApiName: string;
17
- recordId: string;
18
- }>();
19
- const navigate = useNavigate();
20
- const objectApiName = objectApiNameParam ?? OBJECT_API_NAMES[0];
21
-
22
- const { layout, record, objectMetadata, loading, error } = useRecordDetailLayout({
23
- objectApiName,
24
- recordId: recordId ?? null,
25
- });
26
-
27
- const recordTitle = useMemo(
28
- () =>
29
- record
30
- ? getGraphQLRecordDisplayName(record, toRecordDisplayNameMetadata(objectMetadata))
31
- : DEFAULT_DETAIL_PAGE_TITLE,
32
- [record, objectMetadata],
33
- );
34
-
35
- const handleBack = () => navigate(-1);
36
-
37
- if (loading) {
38
- return (
39
- <div
40
- className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12"
41
- role="status"
42
- aria-live="polite"
43
- aria-label="Loading record details"
44
- >
45
- <span className="sr-only">Loading record details</span>
46
- <Skeleton className="h-10 w-32 mb-6" aria-hidden="true" />
47
- <Card aria-hidden="true">
48
- <CardHeader>
49
- <Skeleton className="h-8 w-3/4" />
50
- </CardHeader>
51
- <CardContent className="space-y-4">
52
- {[1, 2, 3, 4].map((i) => (
53
- <div key={i} className="space-y-2">
54
- <Skeleton className="h-4 w-24" />
55
- <Skeleton className="h-4 w-full" />
56
- </div>
57
- ))}
58
- </CardContent>
59
- </Card>
60
- </div>
61
- );
62
- }
63
-
64
- if (error) {
65
- return (
66
- <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
67
- <DetailHeader title="" onBack={handleBack} />
68
- <Alert variant="destructive" role="alert">
69
- <AlertCircle className="h-4 w-4" aria-hidden="true" />
70
- <AlertTitle>Error</AlertTitle>
71
- <AlertDescription>{error}</AlertDescription>
72
- </Alert>
73
- </div>
74
- );
75
- }
76
-
77
- if (!layout || !record) {
78
- return (
79
- <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
80
- <DetailHeader title="" onBack={handleBack} />
81
- <Alert role="alert">
82
- <AlertCircle className="h-4 w-4" aria-hidden="true" />
83
- <AlertTitle>Not Found</AlertTitle>
84
- <AlertDescription>Record not found</AlertDescription>
85
- </Alert>
86
- </div>
87
- );
88
- }
89
-
90
- return (
91
- <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12" aria-label="Record details">
92
- <DetailHeader title={recordTitle} onBack={handleBack} />
93
- <Card>
94
- <CardHeader>
95
- <CardTitle className="text-2xl">{recordTitle}</CardTitle>
96
- </CardHeader>
97
- <CardContent>
98
- <UiApiDetailForm
99
- objectApiName={objectApiName}
100
- recordId={recordId!}
101
- layout={layout}
102
- record={record}
103
- objectMetadata={objectMetadata}
104
- />
105
- </CardContent>
106
- </Card>
107
- </main>
108
- );
109
- }
@@ -1,235 +0,0 @@
1
- /**
2
- * GlobalSearch Page Component
3
- *
4
- * Main page component for displaying global search results.
5
- * Uses GraphQL API (useRecordListGraphQL) for list data; results are adapted to the
6
- * same record shape as before so SearchResultCard and filters/sort/pagination work unchanged.
7
- *
8
- * @remarks
9
- * - Supports single object search (no tabs)
10
- * - Displays filters panel on the left and results on the right
11
- * - Pagination uses a cursor stack: we only query forward (first + after) and store endCursor per page;
12
- * Previous re-queries using the stored cursor for the previous page so both Next and Previous work.
13
- */
14
- import { useMemo, useState, useCallback, useEffect, useRef } from "react";
15
- import { useParams } from "react-router";
16
- import { OBJECT_API_NAMES, DEFAULT_PAGE_SIZE } from "../constants";
17
- import { useObjectListMetadata } from "../hooks/useObjectSearchData";
18
- import { useObjectInfoBatch } from "../hooks/useObjectInfoBatch";
19
- import { useRecordListGraphQL } from "../hooks/useRecordListGraphQL";
20
- import FiltersPanel from "../components/filters/FiltersPanel";
21
- import SearchHeader from "../components/search/SearchHeader";
22
- import SearchResultsPanel from "../components/search/SearchResultsPanel";
23
- import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/card";
24
- import { Skeleton } from "../../../components/ui/skeleton";
25
- import type { FilterCriteria } from "../types/filters/filters";
26
- import type { SearchResultRecord } from "../types/search/searchResults";
27
- import { graphQLNodeToSearchResultRecordData } from "../utils/graphQLRecordAdapter";
28
-
29
- const EMPTY_HIGHLIGHT = { fields: {}, snippet: null };
30
- const EMPTY_SEARCH_INFO = { isPromoted: false, isSpellCorrected: false };
31
-
32
- export default function GlobalSearch() {
33
- const { query } = useParams<{ query: string }>();
34
-
35
- const objectApiName = OBJECT_API_NAMES[0];
36
-
37
- const [searchPageSize, setSearchPageSize] = useState(DEFAULT_PAGE_SIZE);
38
- const [afterCursor, setAfterCursor] = useState<string | null>(null);
39
- const [pageIndex, setPageIndex] = useState(0);
40
- /** Cursor stack: cursorStack[i] is the `after` value that returns page i. cursorStack[0] = null (first page). */
41
- const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);
42
- const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
43
- const [sortBy, setSortBy] = useState("Name");
44
-
45
- const decodedQuery = useMemo(() => {
46
- if (!query) return "";
47
- try {
48
- return decodeURIComponent(query);
49
- } catch {
50
- return query;
51
- }
52
- }, [query]);
53
-
54
- const isBrowseAll = decodedQuery === "browse__all";
55
- const searchQuery = isBrowseAll ? "" : decodedQuery.trim();
56
-
57
- // Reset pagination when the URL search query changes so we don't use an old cursor with a new result set
58
- useEffect(() => {
59
- queueMicrotask(() => {
60
- setAfterCursor(null);
61
- setPageIndex(0);
62
- setCursorStack([null]);
63
- });
64
- }, [query]);
65
-
66
- const listMeta = useObjectListMetadata(objectApiName);
67
- const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
68
- const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? "records";
69
- const {
70
- edges,
71
- pageInfo,
72
- loading: resultsLoading,
73
- error: resultsError,
74
- } = useRecordListGraphQL({
75
- objectApiName,
76
- first: searchPageSize,
77
- after: afterCursor,
78
- filters: appliedFilters,
79
- sortBy: sortBy === "relevance" ? "Name" : sortBy,
80
- searchQuery: searchQuery || undefined,
81
- columns: listMeta.columns,
82
- columnsLoading: listMeta.loading,
83
- columnsError: listMeta.error,
84
- });
85
-
86
- // Store endCursor for the next page so we can re-query when user clicks Next; also enables Previous via stack.
87
- // Only update when not loading so a stale response cannot write a cursor into the wrong stack index (e.g. after rapid Next clicks).
88
- useEffect(() => {
89
- if (resultsLoading) return;
90
- const cursor = pageInfo?.endCursor ?? null;
91
- if (cursor == null) return;
92
- queueMicrotask(() => {
93
- setCursorStack((prev) => {
94
- const next = [...prev];
95
- next[pageIndex + 1] = cursor;
96
- return next;
97
- });
98
- });
99
- }, [resultsLoading, pageInfo?.endCursor, pageIndex]);
100
-
101
- const results: SearchResultRecord[] = useMemo(
102
- () =>
103
- (edges ?? []).map((edge) => ({
104
- record: graphQLNodeToSearchResultRecordData(
105
- edge?.node as Record<string, unknown>,
106
- objectApiName,
107
- ),
108
- highlightInfo: EMPTY_HIGHLIGHT,
109
- searchInfo: EMPTY_SEARCH_INFO,
110
- })),
111
- [edges, objectApiName],
112
- );
113
-
114
- const nextPageToken = pageInfo?.endCursor ?? null;
115
- /** Entry cursor for the previous page; used when user clicks Previous to re-query with after=cursorStack[pageIndex-1]. */
116
- const previousPageToken = pageIndex > 0 ? (cursorStack[pageIndex - 1] ?? null) : null;
117
- const hasNextPage = pageInfo?.hasNextPage === true;
118
- const hasPreviousPage = pageIndex > 0;
119
- const currentPageToken = pageIndex.toString();
120
-
121
- const cursorStackRef = useRef(cursorStack);
122
- const pageIndexRef = useRef(pageIndex);
123
- useEffect(() => {
124
- cursorStackRef.current = cursorStack;
125
- pageIndexRef.current = pageIndex;
126
- }, [cursorStack, pageIndex]);
127
-
128
- const canRenderFilters =
129
- !listMeta.loading && listMeta.filters !== undefined && listMeta.picklistValues !== undefined;
130
-
131
- const handleApplyFilters = useCallback((filterCriteria: FilterCriteria[]) => {
132
- setAppliedFilters(filterCriteria);
133
- setAfterCursor(null);
134
- setPageIndex(0);
135
- setCursorStack([null]);
136
- }, []);
137
-
138
- const handlePageChange = useCallback(
139
- (newPageToken: string, direction?: "next" | "prev" | "first") => {
140
- if (direction === "first" || newPageToken === "0") {
141
- setAfterCursor(null);
142
- setPageIndex(0);
143
- } else if (direction === "prev") {
144
- const idx = pageIndexRef.current;
145
- const stack = cursorStackRef.current;
146
- const prevCursor = idx > 0 ? (stack[idx - 1] ?? null) : null;
147
- setAfterCursor(prevCursor);
148
- setPageIndex((prev) => Math.max(0, prev - 1));
149
- } else {
150
- setAfterCursor(newPageToken);
151
- setPageIndex((prev) => prev + 1);
152
- }
153
- window.scrollTo({ top: 0, behavior: "smooth" });
154
- },
155
- [],
156
- );
157
-
158
- const handlePageSizeChange = useCallback((newPageSize: number) => {
159
- setSearchPageSize(newPageSize);
160
- setAfterCursor(null);
161
- setPageIndex(0);
162
- setCursorStack([null]);
163
- window.scrollTo({ top: 0, behavior: "smooth" });
164
- }, []);
165
-
166
- const handleSortByChange = useCallback((newSortBy: string) => {
167
- setSortBy(newSortBy);
168
- setAfterCursor(null);
169
- setPageIndex(0);
170
- setCursorStack([null]);
171
- window.scrollTo({ top: 0, behavior: "smooth" });
172
- }, []);
173
-
174
- return (
175
- <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
176
- <SearchHeader query={decodedQuery} isBrowseAll={isBrowseAll} labelPlural={labelPlural} />
177
-
178
- <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
179
- <aside className="lg:col-span-1" aria-label="Filters panel">
180
- {canRenderFilters ? (
181
- <FiltersPanel
182
- filters={listMeta.filters}
183
- picklistValues={listMeta.picklistValues}
184
- loading={listMeta.loading}
185
- objectApiName={objectApiName}
186
- onApplyFilters={handleApplyFilters}
187
- />
188
- ) : (
189
- <Card className="w-full" role="region" aria-label="Filters panel">
190
- <CardHeader>
191
- <CardTitle>Filters</CardTitle>
192
- </CardHeader>
193
- <CardContent
194
- className="space-y-4"
195
- role="status"
196
- aria-live="polite"
197
- aria-label="Loading filters"
198
- >
199
- <span className="sr-only">Loading filters</span>
200
- {[1, 2, 3].map((i) => (
201
- <div key={i} className="space-y-2" aria-hidden="true">
202
- <Skeleton className="h-4 w-24" />
203
- <Skeleton className="h-9 w-full" />
204
- </div>
205
- ))}
206
- </CardContent>
207
- </Card>
208
- )}
209
- </aside>
210
-
211
- <section className="lg:col-span-3" aria-label="Search results">
212
- <SearchResultsPanel
213
- objectApiName={objectApiName}
214
- columns={listMeta.columns}
215
- results={results}
216
- columnsLoading={listMeta.loading}
217
- resultsLoading={resultsLoading}
218
- columnsError={listMeta.error}
219
- resultsError={resultsError}
220
- currentPageToken={currentPageToken}
221
- nextPageToken={nextPageToken}
222
- previousPageToken={previousPageToken}
223
- hasNextPage={hasNextPage}
224
- hasPreviousPage={hasPreviousPage}
225
- pageSize={searchPageSize}
226
- sortBy={sortBy}
227
- onPageChange={handlePageChange}
228
- onPageSizeChange={handlePageSizeChange}
229
- onSortByChange={handleSortByChange}
230
- />
231
- </section>
232
- </div>
233
- </main>
234
- );
235
- }