@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
- package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
- package/dist/package.json +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
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 (e) {
|
|
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
|
+
setAfterCursor(null);
|
|
60
|
+
setPageIndex(0);
|
|
61
|
+
setCursorStack([null]);
|
|
62
|
+
}, [query]);
|
|
63
|
+
|
|
64
|
+
const listMeta = useObjectListMetadata(objectApiName);
|
|
65
|
+
const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
|
|
66
|
+
const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? "records";
|
|
67
|
+
const {
|
|
68
|
+
edges,
|
|
69
|
+
pageInfo,
|
|
70
|
+
loading: resultsLoading,
|
|
71
|
+
error: resultsError,
|
|
72
|
+
} = useRecordListGraphQL({
|
|
73
|
+
objectApiName,
|
|
74
|
+
first: searchPageSize,
|
|
75
|
+
after: afterCursor,
|
|
76
|
+
filters: appliedFilters,
|
|
77
|
+
sortBy: sortBy === "relevance" ? "Name" : sortBy,
|
|
78
|
+
searchQuery: searchQuery || undefined,
|
|
79
|
+
columns: listMeta.columns,
|
|
80
|
+
columnsLoading: listMeta.loading,
|
|
81
|
+
columnsError: listMeta.error,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Store endCursor for the next page so we can re-query when user clicks Next; also enables Previous via stack.
|
|
85
|
+
// Only update when not loading so a stale response cannot write a cursor into the wrong stack index (e.g. after rapid Next clicks).
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (resultsLoading) return;
|
|
88
|
+
const cursor = pageInfo?.endCursor ?? null;
|
|
89
|
+
if (cursor == null) return;
|
|
90
|
+
setCursorStack((prev) => {
|
|
91
|
+
const next = [...prev];
|
|
92
|
+
next[pageIndex + 1] = cursor;
|
|
93
|
+
return next;
|
|
94
|
+
});
|
|
95
|
+
}, [resultsLoading, pageInfo?.endCursor, pageIndex]);
|
|
96
|
+
|
|
97
|
+
const results: SearchResultRecord[] = useMemo(
|
|
98
|
+
() =>
|
|
99
|
+
(edges ?? []).map((edge) => ({
|
|
100
|
+
record: graphQLNodeToSearchResultRecordData(
|
|
101
|
+
edge?.node as Record<string, unknown>,
|
|
102
|
+
objectApiName,
|
|
103
|
+
),
|
|
104
|
+
highlightInfo: EMPTY_HIGHLIGHT,
|
|
105
|
+
searchInfo: EMPTY_SEARCH_INFO,
|
|
106
|
+
})),
|
|
107
|
+
[edges, objectApiName],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const nextPageToken = pageInfo?.endCursor ?? null;
|
|
111
|
+
/** Entry cursor for the previous page; used when user clicks Previous to re-query with after=cursorStack[pageIndex-1]. */
|
|
112
|
+
const previousPageToken = pageIndex > 0 ? (cursorStack[pageIndex - 1] ?? null) : null;
|
|
113
|
+
const hasNextPage = pageInfo?.hasNextPage === true;
|
|
114
|
+
const hasPreviousPage = pageIndex > 0;
|
|
115
|
+
const currentPageToken = pageIndex.toString();
|
|
116
|
+
|
|
117
|
+
const cursorStackRef = useRef(cursorStack);
|
|
118
|
+
const pageIndexRef = useRef(pageIndex);
|
|
119
|
+
cursorStackRef.current = cursorStack;
|
|
120
|
+
pageIndexRef.current = pageIndex;
|
|
121
|
+
|
|
122
|
+
const canRenderFilters =
|
|
123
|
+
!listMeta.loading && listMeta.filters !== undefined && listMeta.picklistValues !== undefined;
|
|
124
|
+
|
|
125
|
+
const handleApplyFilters = useCallback((filterCriteria: FilterCriteria[]) => {
|
|
126
|
+
setAppliedFilters(filterCriteria);
|
|
127
|
+
setAfterCursor(null);
|
|
128
|
+
setPageIndex(0);
|
|
129
|
+
setCursorStack([null]);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const handlePageChange = useCallback(
|
|
133
|
+
(newPageToken: string, direction?: "next" | "prev" | "first") => {
|
|
134
|
+
if (direction === "first" || newPageToken === "0") {
|
|
135
|
+
setAfterCursor(null);
|
|
136
|
+
setPageIndex(0);
|
|
137
|
+
} else if (direction === "prev") {
|
|
138
|
+
const idx = pageIndexRef.current;
|
|
139
|
+
const stack = cursorStackRef.current;
|
|
140
|
+
const prevCursor = idx > 0 ? (stack[idx - 1] ?? null) : null;
|
|
141
|
+
setAfterCursor(prevCursor);
|
|
142
|
+
setPageIndex((prev) => Math.max(0, prev - 1));
|
|
143
|
+
} else {
|
|
144
|
+
setAfterCursor(newPageToken);
|
|
145
|
+
setPageIndex((prev) => prev + 1);
|
|
146
|
+
}
|
|
147
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
148
|
+
},
|
|
149
|
+
[],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
|
153
|
+
setSearchPageSize(newPageSize);
|
|
154
|
+
setAfterCursor(null);
|
|
155
|
+
setPageIndex(0);
|
|
156
|
+
setCursorStack([null]);
|
|
157
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const handleSortByChange = useCallback((newSortBy: string) => {
|
|
161
|
+
setSortBy(newSortBy);
|
|
162
|
+
setAfterCursor(null);
|
|
163
|
+
setPageIndex(0);
|
|
164
|
+
setCursorStack([null]);
|
|
165
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
170
|
+
<SearchHeader query={decodedQuery} isBrowseAll={isBrowseAll} labelPlural={labelPlural} />
|
|
171
|
+
|
|
172
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
173
|
+
<aside className="lg:col-span-1" aria-label="Filters panel">
|
|
174
|
+
{canRenderFilters ? (
|
|
175
|
+
<FiltersPanel
|
|
176
|
+
filters={listMeta.filters}
|
|
177
|
+
picklistValues={listMeta.picklistValues}
|
|
178
|
+
loading={listMeta.loading}
|
|
179
|
+
objectApiName={objectApiName}
|
|
180
|
+
onApplyFilters={handleApplyFilters}
|
|
181
|
+
/>
|
|
182
|
+
) : (
|
|
183
|
+
<Card className="w-full" role="region" aria-label="Filters panel">
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardTitle>Filters</CardTitle>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent
|
|
188
|
+
className="space-y-4"
|
|
189
|
+
role="status"
|
|
190
|
+
aria-live="polite"
|
|
191
|
+
aria-label="Loading filters"
|
|
192
|
+
>
|
|
193
|
+
<span className="sr-only">Loading filters</span>
|
|
194
|
+
{[1, 2, 3].map((i) => (
|
|
195
|
+
<div key={i} className="space-y-2" aria-hidden="true">
|
|
196
|
+
<Skeleton className="h-4 w-24" />
|
|
197
|
+
<Skeleton className="h-9 w-full" />
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
</CardContent>
|
|
201
|
+
</Card>
|
|
202
|
+
)}
|
|
203
|
+
</aside>
|
|
204
|
+
|
|
205
|
+
<section className="lg:col-span-3" aria-label="Search results">
|
|
206
|
+
<SearchResultsPanel
|
|
207
|
+
objectApiName={objectApiName}
|
|
208
|
+
columns={listMeta.columns}
|
|
209
|
+
results={results}
|
|
210
|
+
columnsLoading={listMeta.loading}
|
|
211
|
+
resultsLoading={resultsLoading}
|
|
212
|
+
columnsError={listMeta.error}
|
|
213
|
+
resultsError={resultsError}
|
|
214
|
+
currentPageToken={currentPageToken}
|
|
215
|
+
nextPageToken={nextPageToken}
|
|
216
|
+
previousPageToken={previousPageToken}
|
|
217
|
+
hasNextPage={hasNextPage}
|
|
218
|
+
hasPreviousPage={hasPreviousPage}
|
|
219
|
+
pageSize={searchPageSize}
|
|
220
|
+
sortBy={sortBy}
|
|
221
|
+
onPageChange={handlePageChange}
|
|
222
|
+
onPageSizeChange={handlePageSizeChange}
|
|
223
|
+
onSortByChange={handleSortByChange}
|
|
224
|
+
/>
|
|
225
|
+
</section>
|
|
226
|
+
</div>
|
|
227
|
+
</main>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions for filter structures
|
|
5
|
+
* All types are derived from Zod schemas using z.infer for type safety
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Allowed filter operators for Salesforce search queries
|
|
10
|
+
*/
|
|
11
|
+
export const FILTER_OPERATORS = [
|
|
12
|
+
"eq", // Equals
|
|
13
|
+
"ne", // Not equals
|
|
14
|
+
"like", // Pattern matching (contains)
|
|
15
|
+
"in", // In list (multiple values)
|
|
16
|
+
"gt", // Greater than
|
|
17
|
+
"gte", // Greater than or equal
|
|
18
|
+
"lt", // Less than
|
|
19
|
+
"lte", // Less than or equal
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Filter operator type
|
|
24
|
+
*/
|
|
25
|
+
export type FilterOperator = (typeof FILTER_OPERATORS)[number];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Salesforce field path validation regex
|
|
29
|
+
* Validates field paths like:
|
|
30
|
+
* - Simple fields: "Name", "FieldName__c"
|
|
31
|
+
* - Relationship fields: "Account__r.Name", "Owner__r.FieldName__c"
|
|
32
|
+
* - Nested relationships: "Account__r.Owner__r.Name"
|
|
33
|
+
*
|
|
34
|
+
* Pattern explanation:
|
|
35
|
+
* - ^[A-Za-z][A-Za-z0-9_]* - Starts with letter, followed by letters/numbers/underscores
|
|
36
|
+
* - (__[cr])? - Optional relationship suffix (__r or __c)
|
|
37
|
+
* - (\.[A-Za-z][A-Za-z0-9_]*(__[cr])?)* - Optional relationship traversal (dot notation)
|
|
38
|
+
*/
|
|
39
|
+
const SALESFORCE_FIELD_PATH_REGEX =
|
|
40
|
+
/^[A-Za-z][A-Za-z0-9_]*(__[cr])?(\.[A-Za-z][A-Za-z0-9_]*(__[cr])?)*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates Salesforce field path format
|
|
44
|
+
* @param fieldPath - The field path to validate
|
|
45
|
+
* @returns true if valid, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
function isValidSalesforceFieldPath(fieldPath: string): boolean {
|
|
48
|
+
if (!fieldPath || fieldPath.trim().length === 0) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return SALESFORCE_FIELD_PATH_REGEX.test(fieldPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Zod Schema for Filter Attributes
|
|
55
|
+
const FilterAttributesSchema = z.object({
|
|
56
|
+
affordance: z.string().optional(),
|
|
57
|
+
placeholder: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filter attributes containing input-specific properties
|
|
62
|
+
*/
|
|
63
|
+
export type FilterAttributes = z.infer<typeof FilterAttributesSchema>;
|
|
64
|
+
|
|
65
|
+
// Zod Schema for Filter
|
|
66
|
+
const FilterSchema = z.object({
|
|
67
|
+
affordance: z.string(),
|
|
68
|
+
attributes: FilterAttributesSchema.optional(),
|
|
69
|
+
defaultValues: z.array(z.string()).optional(),
|
|
70
|
+
helpMessage: z.string().nullable().optional(),
|
|
71
|
+
label: z.string(),
|
|
72
|
+
targetFieldPath: z.string().refine((value) => isValidSalesforceFieldPath(value), {
|
|
73
|
+
message:
|
|
74
|
+
"Invalid Salesforce field path format. Field paths must start with a letter and can contain letters, numbers, underscores, and relationship notation (__r or __c). Use dot notation for relationships (e.g., 'Account__r.Name').",
|
|
75
|
+
}),
|
|
76
|
+
type: z.string(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Single filter definition from getObjectListFilters API
|
|
81
|
+
*/
|
|
82
|
+
export type Filter = z.infer<typeof FilterSchema>;
|
|
83
|
+
|
|
84
|
+
// Export schema for validation
|
|
85
|
+
export const FilterArraySchema = z.array(FilterSchema);
|
|
86
|
+
|
|
87
|
+
// Zod Schema for Filter Criteria with operator and field path validation
|
|
88
|
+
const FilterCriteriaSchema = z.object({
|
|
89
|
+
objectApiName: z.string().min(1, "Object API name is required"),
|
|
90
|
+
fieldPath: z
|
|
91
|
+
.string()
|
|
92
|
+
.min(1, "Field path is required")
|
|
93
|
+
.refine((value) => isValidSalesforceFieldPath(value), {
|
|
94
|
+
message:
|
|
95
|
+
"Invalid Salesforce field path format. Field paths must start with a letter and can contain letters, numbers, underscores, and relationship notation (__r or __c). Use dot notation for relationships (e.g., 'Account__r.Name').",
|
|
96
|
+
}),
|
|
97
|
+
operator: z.enum(FILTER_OPERATORS, {
|
|
98
|
+
message: `Operator must be one of: ${FILTER_OPERATORS.join(", ")}`,
|
|
99
|
+
}),
|
|
100
|
+
values: z.array(z.union([z.string(), z.number()])).min(1, "At least one value is required"),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Filter criteria structure for filtering search results
|
|
105
|
+
*/
|
|
106
|
+
export type FilterCriteria = z.infer<typeof FilterCriteriaSchema>;
|
|
107
|
+
|
|
108
|
+
// Export schema for validation
|
|
109
|
+
export const FilterCriteriaArraySchema = z.array(FilterCriteriaSchema);
|
|
110
|
+
|
|
111
|
+
// Zod Schema for Filters Response
|
|
112
|
+
const FiltersResponseSchema = z.record(z.string(), z.unknown()).and(
|
|
113
|
+
z.object({
|
|
114
|
+
filters: FilterArraySchema.optional(),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filters response structure
|
|
120
|
+
*/
|
|
121
|
+
export type FiltersResponse = z.infer<typeof FiltersResponseSchema>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions for picklist value structures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Zod Schema for Picklist Value
|
|
8
|
+
const PicklistValueSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
// 1. Cleanup: Attributes are usually an object, not just 'unknown'
|
|
11
|
+
attributes: z.record(z.string(), z.unknown()).nullish(),
|
|
12
|
+
label: z.string(),
|
|
13
|
+
|
|
14
|
+
// 2. Precise Typing: 'validFor' is an array of indices (numbers)
|
|
15
|
+
// pointing to the controlling field's values.
|
|
16
|
+
validFor: z.array(z.number()).nullish(),
|
|
17
|
+
value: z.string(),
|
|
18
|
+
|
|
19
|
+
// 3. Usability: Added common API fields that are useful for UI logic
|
|
20
|
+
// (marked optional in case the specific API version omits them)
|
|
21
|
+
defaultValue: z.boolean().nullish(),
|
|
22
|
+
active: z.boolean().nullish(),
|
|
23
|
+
})
|
|
24
|
+
.passthrough();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Single picklist value from the API
|
|
28
|
+
*/
|
|
29
|
+
export type PicklistValue = z.infer<typeof PicklistValueSchema>;
|
|
30
|
+
|
|
31
|
+
// Export schema for validation
|
|
32
|
+
export const PicklistValueArraySchema = z.array(PicklistValueSchema);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definitions and Zod schemas for Object Info Batch API response
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Zod Schema for Child Relationship
|
|
8
|
+
const ChildRelationshipSchema = z.object({
|
|
9
|
+
childObjectApiName: z.string(),
|
|
10
|
+
fieldName: z.string(),
|
|
11
|
+
junctionIdListNames: z.array(z.string()),
|
|
12
|
+
junctionReferenceTo: z.array(z.string()),
|
|
13
|
+
relationshipName: z.string(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Zod Schema for Reference To Info
|
|
17
|
+
const ReferenceToInfoSchema = z.object({
|
|
18
|
+
apiName: z.string(),
|
|
19
|
+
nameFields: z.array(z.string()),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Zod Schema for Filtered Lookup Info (can be null or object)
|
|
23
|
+
const FilteredLookupInfoSchema = z.record(z.string(), z.unknown()).nullable();
|
|
24
|
+
|
|
25
|
+
// Zod Schema for Field Definition
|
|
26
|
+
// Using passthrough to allow extra fields that might be present in the API response
|
|
27
|
+
const FieldSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
apiName: z.string(),
|
|
30
|
+
calculated: z.boolean(),
|
|
31
|
+
compound: z.boolean(),
|
|
32
|
+
compoundComponentName: z.string().nullable(),
|
|
33
|
+
compoundFieldName: z.string().nullable(),
|
|
34
|
+
controllerName: z.string().nullable(),
|
|
35
|
+
controllingFields: z.array(z.string()),
|
|
36
|
+
createable: z.boolean(),
|
|
37
|
+
custom: z.boolean(),
|
|
38
|
+
dataType: z.string(),
|
|
39
|
+
defaultValue: z.unknown().nullable(),
|
|
40
|
+
defaultedOnCreate: z.boolean(),
|
|
41
|
+
digits: z.number(),
|
|
42
|
+
externalId: z.boolean(),
|
|
43
|
+
extraTypeInfo: z.string().nullable(),
|
|
44
|
+
filterable: z.boolean(),
|
|
45
|
+
filteredLookupInfo: FilteredLookupInfoSchema,
|
|
46
|
+
highScaleNumber: z.boolean(),
|
|
47
|
+
htmlFormatted: z.boolean(),
|
|
48
|
+
inlineHelpText: z.string().nullable(),
|
|
49
|
+
label: z.string(),
|
|
50
|
+
length: z.number(),
|
|
51
|
+
maskType: z.string().nullable(),
|
|
52
|
+
nameField: z.boolean(),
|
|
53
|
+
polymorphicForeignKey: z.boolean(),
|
|
54
|
+
precision: z.number(),
|
|
55
|
+
reference: z.boolean(),
|
|
56
|
+
referenceTargetField: z.string().nullable(),
|
|
57
|
+
referenceToInfos: z.array(ReferenceToInfoSchema),
|
|
58
|
+
relationshipName: z.string().nullable(),
|
|
59
|
+
required: z.boolean(),
|
|
60
|
+
scale: z.number(),
|
|
61
|
+
searchPrefilterable: z.boolean(),
|
|
62
|
+
sortable: z.boolean(),
|
|
63
|
+
unique: z.boolean(),
|
|
64
|
+
updateable: z.boolean(),
|
|
65
|
+
})
|
|
66
|
+
.passthrough();
|
|
67
|
+
|
|
68
|
+
// Zod Schema for Record Type Info
|
|
69
|
+
// Using passthrough to allow extra fields that might be present in the API response
|
|
70
|
+
const RecordTypeInfoSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
available: z.boolean(),
|
|
73
|
+
defaultRecordTypeMapping: z.boolean(),
|
|
74
|
+
master: z.boolean(),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
recordTypeId: z.string(),
|
|
77
|
+
})
|
|
78
|
+
.passthrough();
|
|
79
|
+
|
|
80
|
+
// Zod Schema for Theme Info
|
|
81
|
+
const ThemeInfoSchema = z.object({
|
|
82
|
+
color: z.string(),
|
|
83
|
+
iconUrl: z.string(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Zod Schema for Object Info Result
|
|
87
|
+
// Using passthrough to allow extra fields and using FieldSchema/RecordTypeInfoSchema with passthrough
|
|
88
|
+
const ObjectInfoResultSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
apiName: z.string(),
|
|
91
|
+
associateEntityType: z.string().nullable(),
|
|
92
|
+
associateParentEntity: z.string().nullable(),
|
|
93
|
+
childRelationships: z.array(ChildRelationshipSchema),
|
|
94
|
+
compactLayoutable: z.boolean(),
|
|
95
|
+
createable: z.boolean(),
|
|
96
|
+
custom: z.boolean(),
|
|
97
|
+
defaultRecordTypeId: z.string(),
|
|
98
|
+
deletable: z.boolean(),
|
|
99
|
+
dependentFields: z.record(z.string(), z.unknown()),
|
|
100
|
+
eTag: z.string(),
|
|
101
|
+
feedEnabled: z.boolean(),
|
|
102
|
+
// Avoid using FieldSchema because of performance concerns with validating the high number of fields returned from the Salesforce API and causing the UI to freeze.
|
|
103
|
+
fields: z.record(z.string(), z.any()),
|
|
104
|
+
keyPrefix: z.string(),
|
|
105
|
+
label: z.string(),
|
|
106
|
+
labelPlural: z.string(),
|
|
107
|
+
layoutable: z.boolean(),
|
|
108
|
+
mruEnabled: z.boolean(),
|
|
109
|
+
nameFields: z.array(z.string()),
|
|
110
|
+
queryable: z.boolean(),
|
|
111
|
+
// Avoid using RecordTypeInfoSchema because of performance concerns with validating the high number of fields returned from the Salesforce API and causing the UI to freeze.
|
|
112
|
+
recordTypeInfos: z.record(z.string(), z.any()),
|
|
113
|
+
searchLayoutable: z.boolean(),
|
|
114
|
+
searchable: z.boolean(),
|
|
115
|
+
themeInfo: ThemeInfoSchema,
|
|
116
|
+
updateable: z.boolean(),
|
|
117
|
+
})
|
|
118
|
+
.passthrough();
|
|
119
|
+
|
|
120
|
+
// Zod Schema for Object Info Batch Result Item
|
|
121
|
+
const ObjectInfoBatchResultItemSchema = z.object({
|
|
122
|
+
result: ObjectInfoResultSchema,
|
|
123
|
+
statusCode: z.number(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Zod Schema for Object Info Batch Response (array of items)
|
|
127
|
+
export const ObjectInfoBatchResponseSchema = z.object({
|
|
128
|
+
results: z.array(ObjectInfoBatchResultItemSchema),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// TypeScript Types (inferred from Zod schemas)
|
|
132
|
+
export type ChildRelationship = z.infer<typeof ChildRelationshipSchema>;
|
|
133
|
+
export type ReferenceToInfo = z.infer<typeof ReferenceToInfoSchema>;
|
|
134
|
+
export type FilteredLookupInfo = z.infer<typeof FilteredLookupInfoSchema>;
|
|
135
|
+
export type Field = z.infer<typeof FieldSchema>;
|
|
136
|
+
export type RecordTypeInfo = z.infer<typeof RecordTypeInfoSchema>;
|
|
137
|
+
export type ThemeInfo = z.infer<typeof ThemeInfoSchema>;
|
|
138
|
+
export type ObjectInfoBatchResponse = z.infer<typeof ObjectInfoBatchResponseSchema>;
|
|
139
|
+
// Type Patching: Overwriting the "any" from the performance-optimized schema
|
|
140
|
+
// with the strict types defined above. This ensures Developers get strict typing
|
|
141
|
+
// even though the Runtime Validator skips the deep check.
|
|
142
|
+
export type ObjectInfoResult = Omit<
|
|
143
|
+
z.infer<typeof ObjectInfoResultSchema>,
|
|
144
|
+
"fields" | "recordTypeInfos"
|
|
145
|
+
> & {
|
|
146
|
+
fields: Record<string, Field>;
|
|
147
|
+
recordTypeInfos: Record<string, RecordTypeInfo>;
|
|
148
|
+
};
|
|
149
|
+
export type ObjectInfoBatchResultItem = Omit<
|
|
150
|
+
z.infer<typeof ObjectInfoBatchResultItemSchema>,
|
|
151
|
+
"result"
|
|
152
|
+
> & {
|
|
153
|
+
result: ObjectInfoResult;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Export schemas for validation
|
|
157
|
+
export {
|
|
158
|
+
ChildRelationshipSchema,
|
|
159
|
+
ReferenceToInfoSchema,
|
|
160
|
+
FilteredLookupInfoSchema,
|
|
161
|
+
FieldSchema,
|
|
162
|
+
RecordTypeInfoSchema,
|
|
163
|
+
ThemeInfoSchema,
|
|
164
|
+
ObjectInfoResultSchema,
|
|
165
|
+
ObjectInfoBatchResultItemSchema,
|
|
166
|
+
};
|