@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.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/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record GraphQL Service
|
|
3
|
+
*
|
|
4
|
+
* Single service for querying Salesforce object records via GraphQL UI API (uiapi.query).
|
|
5
|
+
* Handles both list (paginated, filter, sort, search) and single-record-by-id using one query shape.
|
|
6
|
+
*
|
|
7
|
+
* @module api/recordListGraphQLService
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDataSDK } from "@salesforce/sdk-data";
|
|
11
|
+
import type { Column } from "../types/search/searchResults";
|
|
12
|
+
import type { FilterCriteria } from "../types/filters/filters";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
15
|
+
|
|
16
|
+
/** Tree of selection: leaf is "value", branch is nested fields. Keys starting with __on_ are inline fragments (e.g. __on_User). */
|
|
17
|
+
interface SelectionTree {
|
|
18
|
+
[key: string]: "value" | SelectionTree;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Polymorphic relationship fields and the concrete GraphQL types they can resolve to.
|
|
23
|
+
* Only relationship names listed here use inline fragments; others use direct selection
|
|
24
|
+
* (e.g. Parent -> Parent { Name { value } } because "Parent" is not a schema type).
|
|
25
|
+
* We use a single fragment per field to avoid schema validation errors (e.g. User and Group
|
|
26
|
+
* cannot both be spread in the same selection in some contexts).
|
|
27
|
+
*/
|
|
28
|
+
const POLYMORPHIC_RELATIONSHIP_TYPES: Record<string, string[]> = {
|
|
29
|
+
Owner: ["User"],
|
|
30
|
+
CreatedBy: ["User"],
|
|
31
|
+
LastModifiedBy: ["User"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds a selection tree from columns (fieldApiName). Simple fields (e.g. Name, OwnerId) become
|
|
36
|
+
* top-level leaves. Relationship fields that are in POLYMORPHIC_RELATIONSHIP_TYPES use a single
|
|
37
|
+
* inline fragment on the first concrete type (e.g. ... on User). All other relationship fields
|
|
38
|
+
* (e.g. Parent, Account) use direct selection (e.g. Parent { Name { value } }) because the
|
|
39
|
+
* relationship name is not necessarily a GraphQL type name (e.g. Parent resolves to Account).
|
|
40
|
+
*/
|
|
41
|
+
function buildSelectionTree(columns: Column[]): SelectionTree {
|
|
42
|
+
const allFieldNames = new Set(columns.map((c) => (c.fieldApiName ?? "").trim()).filter(Boolean));
|
|
43
|
+
const tree: SelectionTree = { Id: "value" };
|
|
44
|
+
for (const col of columns) {
|
|
45
|
+
const name = (col.fieldApiName ?? "").trim();
|
|
46
|
+
if (!name) continue;
|
|
47
|
+
const parts = name.split(".");
|
|
48
|
+
if (parts.length === 1) {
|
|
49
|
+
const fieldName = parts[0];
|
|
50
|
+
const hasCorrespondingId = allFieldNames.has(`${fieldName}Id`);
|
|
51
|
+
if (hasCorrespondingId) {
|
|
52
|
+
const knownTypes = POLYMORPHIC_RELATIONSHIP_TYPES[fieldName];
|
|
53
|
+
if (knownTypes?.length) {
|
|
54
|
+
// Use a single inline fragment to avoid "User can never be Group" validation errors
|
|
55
|
+
const typeName = knownTypes[0];
|
|
56
|
+
tree[fieldName] = { [`__on_${typeName}`]: { Name: "value" } };
|
|
57
|
+
} else {
|
|
58
|
+
// Relationship name (e.g. Parent) is not a GraphQL type; use direct selection
|
|
59
|
+
tree[fieldName] = { Name: "value" };
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
tree[fieldName] = "value";
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
let current = tree;
|
|
66
|
+
for (let i = 0; i < parts.length; i++) {
|
|
67
|
+
const part = parts[i];
|
|
68
|
+
const isLeaf = i === parts.length - 1;
|
|
69
|
+
if (isLeaf) {
|
|
70
|
+
current[part] = "value";
|
|
71
|
+
} else {
|
|
72
|
+
const existing = current[part];
|
|
73
|
+
if (existing === "value") continue;
|
|
74
|
+
if (!existing) {
|
|
75
|
+
current[part] = {};
|
|
76
|
+
}
|
|
77
|
+
current = current[part] as SelectionTree;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Serializes a selection tree to GraphQL selection set string.
|
|
87
|
+
* Keys starting with __on_ are emitted as inline fragments (... on TypeName { ... }).
|
|
88
|
+
* Id is scalar (no subselection); other leaves use { value }.
|
|
89
|
+
*/
|
|
90
|
+
function serializeSelectionTree(tree: SelectionTree, indent: string): string {
|
|
91
|
+
const fragmentKeys = Object.keys(tree).filter((k) => k.startsWith("__on_"));
|
|
92
|
+
const normalKeys = Object.keys(tree).filter((k) => !k.startsWith("__on_"));
|
|
93
|
+
normalKeys.sort((a, b) => {
|
|
94
|
+
if (a === "Id") return -1;
|
|
95
|
+
if (b === "Id") return 1;
|
|
96
|
+
return a.localeCompare(b);
|
|
97
|
+
});
|
|
98
|
+
fragmentKeys.sort();
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
const childIndent = `${indent} `;
|
|
101
|
+
for (const key of normalKeys) {
|
|
102
|
+
const val = tree[key];
|
|
103
|
+
if (val === "value") {
|
|
104
|
+
if (key === "Id") {
|
|
105
|
+
lines.push(`${indent}Id`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(`${indent}${key} { value }`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(`${indent}${key} {`);
|
|
111
|
+
lines.push(serializeSelectionTree(val, childIndent));
|
|
112
|
+
lines.push(`${indent}}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const key of fragmentKeys) {
|
|
116
|
+
const typeName = key.slice(5);
|
|
117
|
+
const val = tree[key];
|
|
118
|
+
if (val && typeof val === "object") {
|
|
119
|
+
lines.push(`${indent}... on ${typeName} {`);
|
|
120
|
+
lines.push(serializeSelectionTree(val, childIndent));
|
|
121
|
+
lines.push(`${indent}}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildNodeSelection(columns: Column[]): string {
|
|
128
|
+
const tree = buildSelectionTree(columns);
|
|
129
|
+
return serializeSelectionTree(tree, " ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Builds GraphQL where clause from filter criteria and optional search text.
|
|
134
|
+
* Search text is applied as Name like %query%. Multiple conditions are combined with and.
|
|
135
|
+
*
|
|
136
|
+
* @param criteria - Field filters (fieldPath, operator, values).
|
|
137
|
+
* @param searchQuery - Optional; adds Name like %searchQuery% when provided.
|
|
138
|
+
*/
|
|
139
|
+
export function buildWhereFromCriteria(
|
|
140
|
+
criteria: FilterCriteria[],
|
|
141
|
+
searchQuery?: string,
|
|
142
|
+
): Record<string, unknown> | null {
|
|
143
|
+
const conditions: Record<string, unknown>[] = [];
|
|
144
|
+
|
|
145
|
+
if (searchQuery && searchQuery.trim()) {
|
|
146
|
+
const term = `%${searchQuery.trim()}%`;
|
|
147
|
+
conditions.push({ Name: { like: term } });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const c of criteria) {
|
|
151
|
+
if (!c.fieldPath || !c.operator || !c.values?.length) continue;
|
|
152
|
+
const value = c.values[0];
|
|
153
|
+
const op = c.operator;
|
|
154
|
+
const parts = c.fieldPath.split(".");
|
|
155
|
+
const fieldClause = { [op]: value };
|
|
156
|
+
if (parts.length === 1) {
|
|
157
|
+
conditions.push({ [parts[0]]: fieldClause });
|
|
158
|
+
} else {
|
|
159
|
+
let nested: Record<string, unknown> = { [parts[parts.length - 1]]: fieldClause };
|
|
160
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
161
|
+
nested = { [parts[i]]: nested };
|
|
162
|
+
}
|
|
163
|
+
conditions.push(nested);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (conditions.length === 0) return null;
|
|
168
|
+
if (conditions.length === 1) return conditions[0] as Record<string, unknown>;
|
|
169
|
+
return { and: conditions };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parses sortBy string (e.g. "Name", "Name ASC", "AnnualRevenue DESC") into GraphQL orderBy shape.
|
|
174
|
+
* Default direction is ASC.
|
|
175
|
+
*/
|
|
176
|
+
export function buildOrderByFromSort(
|
|
177
|
+
sortBy: string,
|
|
178
|
+
): Record<string, { order: "ASC" | "DESC" }> | null {
|
|
179
|
+
const trimmed = (sortBy ?? "").trim();
|
|
180
|
+
if (!trimmed || trimmed.toLowerCase() === "relevance") return null;
|
|
181
|
+
const parts = trimmed.split(/\s+/);
|
|
182
|
+
const field = parts[0];
|
|
183
|
+
const dir = parts[1]?.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
184
|
+
return { [field]: { order: dir } };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Variables for the GetRecords GraphQL operation. */
|
|
188
|
+
export interface RecordListGraphQLVariables {
|
|
189
|
+
first?: number;
|
|
190
|
+
after?: string | null;
|
|
191
|
+
where?: Record<string, unknown> | null;
|
|
192
|
+
orderBy?: Record<string, unknown> | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface RecordListGraphQLOptions {
|
|
196
|
+
objectApiName: string;
|
|
197
|
+
columns: Column[];
|
|
198
|
+
/** When set, fetches a single record by Id (first=1, where Id eq); used for detail view. */
|
|
199
|
+
recordId?: string | null;
|
|
200
|
+
first?: number;
|
|
201
|
+
after?: string | null;
|
|
202
|
+
filters?: FilterCriteria[];
|
|
203
|
+
orderBy?: Record<string, unknown> | null;
|
|
204
|
+
searchQuery?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Builds the GraphQL query string for uiapi.query.{objectApiName}.
|
|
209
|
+
* Used for both list (pagination, where, orderBy) and single record (where Id eq, first 1).
|
|
210
|
+
*
|
|
211
|
+
* @param objectApiName - API name of the object (e.g. Account, Contact).
|
|
212
|
+
* @param columns - Field selection (becomes node selection via buildNodeSelection).
|
|
213
|
+
* @param options - Optional where and orderBy; when recordId is used, where is set to Id eq.
|
|
214
|
+
*/
|
|
215
|
+
export function buildGetRecordsQuery(
|
|
216
|
+
objectApiName: string,
|
|
217
|
+
columns: Column[],
|
|
218
|
+
options?: { where?: Record<string, unknown> | null; orderBy?: Record<string, unknown> | null },
|
|
219
|
+
): string {
|
|
220
|
+
const nodeSelection = buildNodeSelection(columns);
|
|
221
|
+
const hasWhere = options?.where != null && Object.keys(options.where).length > 0;
|
|
222
|
+
const hasOrderBy = options?.orderBy != null && Object.keys(options.orderBy).length > 0;
|
|
223
|
+
|
|
224
|
+
const filterType = `${objectApiName}_Filter`;
|
|
225
|
+
const orderByType = `${objectApiName}_OrderBy`;
|
|
226
|
+
|
|
227
|
+
const varDecls = [
|
|
228
|
+
"$first: Int",
|
|
229
|
+
"$after: String",
|
|
230
|
+
...(hasWhere ? [`$where: ${filterType}`] : []),
|
|
231
|
+
...(hasOrderBy ? [`$orderBy: ${orderByType}`] : []),
|
|
232
|
+
];
|
|
233
|
+
const opArgs = [
|
|
234
|
+
"first: $first",
|
|
235
|
+
"after: $after",
|
|
236
|
+
...(hasWhere ? ["where: $where"] : []),
|
|
237
|
+
...(hasOrderBy ? ["orderBy: $orderBy"] : []),
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
return `query GetRecords(${varDecls.join(", ")}) {
|
|
241
|
+
uiapi {
|
|
242
|
+
query {
|
|
243
|
+
${objectApiName}(${opArgs.join(", ")}) {
|
|
244
|
+
edges {
|
|
245
|
+
node {
|
|
246
|
+
${nodeSelection}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
pageInfo {
|
|
250
|
+
hasNextPage
|
|
251
|
+
hasPreviousPage
|
|
252
|
+
endCursor
|
|
253
|
+
startCursor
|
|
254
|
+
},
|
|
255
|
+
totalCount,
|
|
256
|
+
pageResultCount
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface RecordListGraphQLResult {
|
|
264
|
+
uiapi?: {
|
|
265
|
+
query?: {
|
|
266
|
+
[key: string]: {
|
|
267
|
+
edges?: Array<{ node?: Record<string, unknown> }>;
|
|
268
|
+
pageInfo?: {
|
|
269
|
+
hasNextPage?: boolean;
|
|
270
|
+
hasPreviousPage?: boolean;
|
|
271
|
+
endCursor?: string | null;
|
|
272
|
+
startCursor?: string | null;
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** GraphQL node shape for a single record (Id + field selections with value/nested). */
|
|
280
|
+
export type GraphQLRecordNode = Record<string, unknown>;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Fetches records for the given object via GraphQL (single query for both list and single record).
|
|
284
|
+
*
|
|
285
|
+
* - List: pass first, after, filters, orderBy, searchQuery.
|
|
286
|
+
* - Single record: pass recordId; first is set to 1 and where includes Id eq.
|
|
287
|
+
*
|
|
288
|
+
* @param options.objectApiName - API name of the object (e.g. Account, Contact).
|
|
289
|
+
* @param options.columns - Field selection (from filters-derived columns or layout-derived optionalFields).
|
|
290
|
+
* @param options.recordId - If set, fetches one record by Id (first=1, where Id eq).
|
|
291
|
+
* @param options.first - Page size (default 50; ignored when recordId is set).
|
|
292
|
+
* @param options.after - Cursor for next page.
|
|
293
|
+
* @param options.filters - Filter criteria (mapped to where).
|
|
294
|
+
* @param options.orderBy - GraphQL orderBy; use buildOrderByFromSort(sortBy) when needed.
|
|
295
|
+
* @param options.searchQuery - Text search (Name like %query% in where).
|
|
296
|
+
* @returns Connection result (edges, pageInfo); for recordId callers use edges[0].node.
|
|
297
|
+
*/
|
|
298
|
+
export async function getRecordsGraphQL(
|
|
299
|
+
options: RecordListGraphQLOptions,
|
|
300
|
+
): Promise<RecordListGraphQLResult> {
|
|
301
|
+
const {
|
|
302
|
+
objectApiName,
|
|
303
|
+
columns,
|
|
304
|
+
recordId,
|
|
305
|
+
first = DEFAULT_PAGE_SIZE,
|
|
306
|
+
after = null,
|
|
307
|
+
filters = [],
|
|
308
|
+
orderBy = null,
|
|
309
|
+
searchQuery,
|
|
310
|
+
} = options;
|
|
311
|
+
|
|
312
|
+
const listWhere = buildWhereFromCriteria(filters, searchQuery);
|
|
313
|
+
const where =
|
|
314
|
+
recordId != null && recordId !== ""
|
|
315
|
+
? listWhere != null && Object.keys(listWhere).length > 0
|
|
316
|
+
? { and: [{ Id: { eq: recordId } }, listWhere] }
|
|
317
|
+
: { Id: { eq: recordId } }
|
|
318
|
+
: listWhere;
|
|
319
|
+
const effectiveFirst = recordId != null && recordId !== "" ? 1 : first;
|
|
320
|
+
const hasWhere = where != null && Object.keys(where).length > 0;
|
|
321
|
+
const hasOrderBy = orderBy != null && Object.keys(orderBy).length > 0;
|
|
322
|
+
|
|
323
|
+
const query = buildGetRecordsQuery(objectApiName, columns, { where, orderBy });
|
|
324
|
+
const variables: Record<string, unknown> = {
|
|
325
|
+
first: effectiveFirst,
|
|
326
|
+
after: after ?? null,
|
|
327
|
+
...(hasWhere && where ? { where } : {}),
|
|
328
|
+
...(hasOrderBy && orderBy ? { orderBy } : {}),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const data = await getDataSDK();
|
|
332
|
+
const response = await data.graphql?.<RecordListGraphQLResult>(query, variables);
|
|
333
|
+
|
|
334
|
+
if (response?.errors?.length) {
|
|
335
|
+
const errorMessages = response.errors.map((e) => e.message).join("; ");
|
|
336
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return response?.data ?? ({} as RecordListGraphQLResult);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Fetches a single record by Id. Uses the same GraphQL query as list (getRecordsGraphQL with recordId + first 1).
|
|
344
|
+
*
|
|
345
|
+
* @param objectApiName - API name of the object.
|
|
346
|
+
* @param recordId - Record Id.
|
|
347
|
+
* @param columns - Field selection (e.g. layout-derived optionalFields as Column[]).
|
|
348
|
+
* @returns The record node or null if not found.
|
|
349
|
+
*/
|
|
350
|
+
export async function getRecordByIdGraphQL(
|
|
351
|
+
objectApiName: string,
|
|
352
|
+
recordId: string,
|
|
353
|
+
columns: Column[],
|
|
354
|
+
): Promise<GraphQLRecordNode | null> {
|
|
355
|
+
const result = await getRecordsGraphQL({
|
|
356
|
+
objectApiName,
|
|
357
|
+
columns,
|
|
358
|
+
recordId,
|
|
359
|
+
first: 1,
|
|
360
|
+
});
|
|
361
|
+
const edges =
|
|
362
|
+
result?.uiapi?.query?.[objectApiName]?.edges ??
|
|
363
|
+
([] as Array<{ node?: Record<string, unknown> }>);
|
|
364
|
+
return (edges[0]?.node ?? null) as GraphQLRecordNode | null;
|
|
365
|
+
}
|
|
@@ -2,17 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { Outlet, NavLink } from "react-router";
|
|
3
3
|
import { Button } from "./components/ui/button";
|
|
4
4
|
import { cn } from "./lib/utils";
|
|
5
|
-
import {
|
|
6
|
-
Home,
|
|
7
|
-
Search,
|
|
8
|
-
BarChart3,
|
|
9
|
-
Wrench,
|
|
10
|
-
HelpCircle,
|
|
11
|
-
Menu,
|
|
12
|
-
Heart,
|
|
13
|
-
Bell,
|
|
14
|
-
Building2,
|
|
15
|
-
} from "lucide-react";
|
|
5
|
+
import { Home, Search, BarChart3, Wrench, Menu, Heart, Bell, Building2, Phone } from "lucide-react";
|
|
16
6
|
|
|
17
7
|
const SIDEBAR_WIDTH = 200;
|
|
18
8
|
const FLOAT_INSET = 20;
|
|
@@ -32,7 +22,7 @@ function AppShell() {
|
|
|
32
22
|
type="button"
|
|
33
23
|
variant="ghost"
|
|
34
24
|
size="icon"
|
|
35
|
-
className="min-h-11 min-w-11 text-primary-foreground hover:bg-primary-foreground/20"
|
|
25
|
+
className="min-h-11 min-w-11 cursor-pointer text-primary-foreground transition-colors duration-200 hover:bg-primary-foreground/20"
|
|
36
26
|
aria-label={navHidden ? "Show menu" : "Hide menu"}
|
|
37
27
|
onClick={() => setNavHidden((h) => !h)}
|
|
38
28
|
>
|
|
@@ -48,7 +38,7 @@ function AppShell() {
|
|
|
48
38
|
type="button"
|
|
49
39
|
variant="ghost"
|
|
50
40
|
size="icon"
|
|
51
|
-
className="text-primary-foreground hover:bg-primary-foreground/20"
|
|
41
|
+
className="cursor-pointer text-primary-foreground transition-colors duration-200 hover:bg-primary-foreground/20"
|
|
52
42
|
aria-label="Favorites"
|
|
53
43
|
>
|
|
54
44
|
<Heart className="size-5" aria-hidden />
|
|
@@ -57,7 +47,7 @@ function AppShell() {
|
|
|
57
47
|
type="button"
|
|
58
48
|
variant="ghost"
|
|
59
49
|
size="icon"
|
|
60
|
-
className="text-primary-foreground hover:bg-primary-foreground/20"
|
|
50
|
+
className="cursor-pointer text-primary-foreground transition-colors duration-200 hover:bg-primary-foreground/20"
|
|
61
51
|
aria-label="Notifications"
|
|
62
52
|
>
|
|
63
53
|
<Bell className="size-5" aria-hidden />
|
|
@@ -71,7 +61,7 @@ function AppShell() {
|
|
|
71
61
|
<div className="relative flex min-h-0 flex-1">
|
|
72
62
|
{!navHidden && (
|
|
73
63
|
<nav
|
|
74
|
-
className="absolute left-5 top-5 bottom-5 z-10 flex w-[200px] flex-col items-start gap-1 overflow-hidden rounded-2xl border border-border bg-card p-4 py-2 shadow-
|
|
64
|
+
className="absolute left-5 top-5 bottom-5 z-10 flex w-[200px] flex-col items-start gap-1 overflow-hidden rounded-2xl border border-border bg-card p-4 py-2 shadow-md"
|
|
75
65
|
aria-label="Main navigation"
|
|
76
66
|
>
|
|
77
67
|
<NavLink
|
|
@@ -79,8 +69,8 @@ function AppShell() {
|
|
|
79
69
|
end
|
|
80
70
|
className={({ isActive }) =>
|
|
81
71
|
cn(
|
|
82
|
-
"flex min-h-11 w-full flex-shrink-0 items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors",
|
|
83
|
-
isActive && "bg-primary text-primary-
|
|
72
|
+
"flex min-h-11 w-full flex-shrink-0 cursor-pointer items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors duration-200",
|
|
73
|
+
isActive && "bg-primary/15 text-primary font-medium",
|
|
84
74
|
)
|
|
85
75
|
}
|
|
86
76
|
aria-label="Home"
|
|
@@ -92,8 +82,8 @@ function AppShell() {
|
|
|
92
82
|
to="/properties"
|
|
93
83
|
className={({ isActive }) =>
|
|
94
84
|
cn(
|
|
95
|
-
"flex min-h-11 w-full flex-shrink-0 items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors",
|
|
96
|
-
isActive && "bg-primary text-primary-
|
|
85
|
+
"flex min-h-11 w-full flex-shrink-0 cursor-pointer items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors duration-200",
|
|
86
|
+
isActive && "bg-primary/15 text-primary font-medium",
|
|
97
87
|
)
|
|
98
88
|
}
|
|
99
89
|
aria-label="Property Search"
|
|
@@ -105,8 +95,8 @@ function AppShell() {
|
|
|
105
95
|
to="/dashboard"
|
|
106
96
|
className={({ isActive }) =>
|
|
107
97
|
cn(
|
|
108
|
-
"flex min-h-11 w-full flex-shrink-0 items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors",
|
|
109
|
-
isActive && "bg-primary text-primary-
|
|
98
|
+
"flex min-h-11 w-full flex-shrink-0 cursor-pointer items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors duration-200",
|
|
99
|
+
isActive && "bg-primary/15 text-primary font-medium",
|
|
110
100
|
)
|
|
111
101
|
}
|
|
112
102
|
aria-label="Dashboard"
|
|
@@ -118,8 +108,8 @@ function AppShell() {
|
|
|
118
108
|
to="/maintenance"
|
|
119
109
|
className={({ isActive }) =>
|
|
120
110
|
cn(
|
|
121
|
-
"flex min-h-11 w-full flex-shrink-0 items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors",
|
|
122
|
-
isActive && "bg-primary text-primary-
|
|
111
|
+
"flex min-h-11 w-full flex-shrink-0 cursor-pointer items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors duration-200",
|
|
112
|
+
isActive && "bg-primary/15 text-primary font-medium",
|
|
123
113
|
)
|
|
124
114
|
}
|
|
125
115
|
aria-label="Maintenance"
|
|
@@ -128,22 +118,22 @@ function AppShell() {
|
|
|
128
118
|
<span className="text-sm font-medium">Maintenance</span>
|
|
129
119
|
</NavLink>
|
|
130
120
|
<NavLink
|
|
131
|
-
to="/
|
|
121
|
+
to="/contact"
|
|
132
122
|
className={({ isActive }) =>
|
|
133
123
|
cn(
|
|
134
|
-
"flex min-h-11 w-full flex-shrink-0 items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors",
|
|
135
|
-
isActive && "bg-primary text-primary-
|
|
124
|
+
"flex min-h-11 w-full flex-shrink-0 cursor-pointer items-center justify-start gap-3 rounded-xl px-3 py-2 text-muted-foreground no-underline transition-colors duration-200",
|
|
125
|
+
isActive && "bg-primary/15 text-primary font-medium",
|
|
136
126
|
)
|
|
137
127
|
}
|
|
138
|
-
aria-label="
|
|
128
|
+
aria-label="Contact"
|
|
139
129
|
>
|
|
140
|
-
<
|
|
141
|
-
<span className="text-sm font-medium">
|
|
130
|
+
<Phone className="size-[22px] shrink-0" aria-hidden />
|
|
131
|
+
<span className="text-sm font-medium">Contact</span>
|
|
142
132
|
</NavLink>
|
|
143
133
|
</nav>
|
|
144
134
|
)}
|
|
145
135
|
<main
|
|
146
|
-
className="min-h-full flex-1 overflow-auto bg-muted/
|
|
136
|
+
className="min-h-full flex-1 overflow-auto bg-muted/40 p-6 transition-[margin-left] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)]"
|
|
147
137
|
style={{
|
|
148
138
|
marginLeft: navHidden ? 0 : FLOAT_INSET + SIDEBAR_WIDTH + FLOAT_GAP,
|
|
149
139
|
}}
|