@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.1 → 1.70.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.
Files changed (131) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Lease__c.json +13 -0
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
  126. package/dist/package.json +1 -1
  127. package/package.json +2 -2
  128. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
  129. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
  130. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
  131. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.70.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.69.0...v1.70.0) (2026-03-04)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ # [1.69.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.68.1...v1.69.0) (2026-03-04)
15
+
16
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
17
+
18
+
19
+
20
+
21
+
6
22
  ## [1.68.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.68.0...v1.68.1) (2026-03-04)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -9424,6 +9424,19 @@
9424
9424
  "Monthly_Rent__c": 23729,
9425
9425
  "Lease_Status__c": "Pending",
9426
9426
  "Security_Deposit__c": 23729
9427
+ },
9428
+ {
9429
+ "attributes": {
9430
+ "type": "Lease__c",
9431
+ "referenceId": "LeaseRef725"
9432
+ },
9433
+ "Property__c": "@PropertyRef12",
9434
+ "Tenant__c": "@TenantRef725",
9435
+ "Start_Date__c": "2025-09-28",
9436
+ "End_Date__c": "2026-03-28",
9437
+ "Monthly_Rent__c": 6728,
9438
+ "Lease_Status__c": "Pending",
9439
+ "Security_Deposit__c": 6728
9427
9440
  }
9428
9441
  ]
9429
9442
  }
@@ -15,20 +15,25 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.68.1",
19
- "@salesforce/webapp-experimental": "^1.68.1",
18
+ "@salesforce/sdk-data": "^1.70.0",
19
+ "@salesforce/webapp-experimental": "^1.70.0",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
- "react": "^19.2.0",
22
- "react-dom": "^19.2.0",
23
- "react-router": "^7.10.1",
24
- "tailwindcss": "^4.1.17",
21
+ "@tanstack/react-form": "^1.28.4",
22
+ "@types/leaflet": "^1.9.21",
25
23
  "class-variance-authority": "^0.7.1",
26
24
  "clsx": "^2.1.1",
25
+ "leaflet": "^1.9.4",
27
26
  "lucide-react": "^0.562.0",
28
27
  "radix-ui": "^1.4.3",
28
+ "react": "^19.2.0",
29
+ "react-dom": "^19.2.0",
30
+ "react-leaflet": "^5.0.0",
31
+ "react-router": "^7.10.1",
29
32
  "shadcn": "^3.8.5",
30
33
  "tailwind-merge": "^3.4.0",
31
- "tw-animate-css": "^1.4.0"
34
+ "tailwindcss": "^4.1.17",
35
+ "tw-animate-css": "^1.4.0",
36
+ "zod": "^4.3.6"
32
37
  },
33
38
  "devDependencies": {
34
39
  "@eslint/js": "^9.39.1",
@@ -38,7 +43,7 @@
38
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
39
44
  "@graphql-tools/utils": "^11.0.0",
40
45
  "@playwright/test": "^1.49.0",
41
- "@salesforce/vite-plugin-webapp-experimental": "^1.68.1",
46
+ "@salesforce/vite-plugin-webapp-experimental": "^1.70.0",
42
47
  "@testing-library/jest-dom": "^6.6.3",
43
48
  "@testing-library/react": "^16.1.0",
44
49
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Create Application__c (property application) record via Salesforce UI API.
3
+ * Uses only fields that exist in all orgs (Property__c, Status__c, Start_Date__c,
4
+ * Employment_Info__c, References__c). Applicant contact details are stored in
5
+ * Employment_Info__c so no data is lost if custom contact fields aren’t deployed.
6
+ */
7
+ import { createRecord } from "@salesforce/webapp-experimental/api";
8
+
9
+ const OBJECT_API_NAME = "Application__c";
10
+
11
+ export interface ApplicationRecordInput {
12
+ Property__c: string | null;
13
+ Status__c?: string;
14
+ First_Name__c?: string | null;
15
+ Last_Name__c?: string | null;
16
+ Email__c?: string | null;
17
+ Phone__c?: string | null;
18
+ Start_Date__c?: string | null;
19
+ Preferred_Term__c?: string | null;
20
+ Employment_Info__c?: string | null;
21
+ References__c?: string | null;
22
+ }
23
+
24
+ function buildEmploymentInfoBlob(input: ApplicationRecordInput): string {
25
+ const lines: string[] = [];
26
+ const contact = [
27
+ input.First_Name__c,
28
+ input.Last_Name__c,
29
+ input.Email__c,
30
+ input.Phone__c,
31
+ input.Preferred_Term__c,
32
+ ]
33
+ .filter(Boolean)
34
+ .join(", ");
35
+ if (contact) lines.push(`Contact: ${contact}`);
36
+ if (input.Employment_Info__c?.trim()) lines.push(input.Employment_Info__c.trim());
37
+ return lines.join("\n\n") || "";
38
+ }
39
+
40
+ /**
41
+ * Creates an Application__c record. Uses only core fields to avoid POST_BODY_PARSE_ERROR
42
+ * when custom contact fields are not yet in the org. Contact + employment text go into Employment_Info__c.
43
+ */
44
+ export async function createApplicationRecord(
45
+ input: ApplicationRecordInput,
46
+ ): Promise<{ id: string }> {
47
+ const fields: Record<string, unknown> = {};
48
+
49
+ if (input.Property__c != null && input.Property__c !== "") {
50
+ fields.Property__c = input.Property__c;
51
+ }
52
+ if (input.Status__c != null && input.Status__c !== "") {
53
+ fields.Status__c = input.Status__c;
54
+ }
55
+ if (input.Start_Date__c != null && input.Start_Date__c !== "") {
56
+ fields.Start_Date__c = input.Start_Date__c;
57
+ }
58
+ const employmentBlob = buildEmploymentInfoBlob(input);
59
+ if (employmentBlob) {
60
+ fields.Employment_Info__c = employmentBlob;
61
+ }
62
+ if (input.References__c != null && input.References__c !== "") {
63
+ fields.References__c = input.References__c;
64
+ }
65
+
66
+ const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
67
+ string,
68
+ unknown
69
+ >;
70
+ const id =
71
+ typeof result.id === "string"
72
+ ? result.id
73
+ : (result.fields as Record<string, { value?: string }> | undefined)?.Id?.value;
74
+ if (!id) {
75
+ throw new Error("Create succeeded but no record id returned");
76
+ }
77
+ return { id };
78
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * GraphQL client using getDataSDK (replaces legacy executeGraphQL from webapp-experimental).
3
+ */
4
+ import { getDataSDK } from "@salesforce/sdk-data";
5
+
6
+ export async function executeGraphQL<T>(
7
+ query: string,
8
+ variables?: Record<string, unknown>,
9
+ ): Promise<T> {
10
+ const data = await getDataSDK();
11
+ const response = await data.graphql?.<T>(query, variables);
12
+ if (response?.errors?.length) {
13
+ const msg = response.errors.map((e) => e.message).join("; ");
14
+ throw new Error(`GraphQL Error: ${msg}`);
15
+ }
16
+ return (response?.data ?? {}) as T;
17
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * API layer: object metadata (GraphQL), layout + record detail (REST layout + GraphQL record), record list/single (GraphQL).
3
+ */
4
+ export { objectInfoService } from "./objectInfoService";
5
+ export { objectDetailService, extractFieldsFromLayout } from "./objectDetailService";
6
+ export type { RecordDetailResult } from "./objectDetailService";
7
+ export {
8
+ getRecordsGraphQL,
9
+ getRecordByIdGraphQL,
10
+ buildGetRecordsQuery,
11
+ buildWhereFromCriteria,
12
+ buildOrderByFromSort,
13
+ } from "./recordListGraphQLService";
14
+ export type {
15
+ RecordListGraphQLResult,
16
+ RecordListGraphQLVariables,
17
+ RecordListGraphQLOptions,
18
+ GraphQLRecordNode,
19
+ } from "./recordListGraphQLService";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Create Lead records in Salesforce for Contact Us form and newsletter signup.
3
+ * Uses the standard Lead object (FirstName, LastName, Email, Phone, Company, Description, LeadSource).
4
+ */
5
+ import { createRecord } from "@salesforce/webapp-experimental/api";
6
+
7
+ const OBJECT_API_NAME = "Lead";
8
+
9
+ const LEAD_SOURCE_WEBSITE = "Website";
10
+ const LEAD_SOURCE_NEWSLETTER = "Website Newsletter";
11
+
12
+ function getRecordIdFromResponse(result: Record<string, unknown>): string {
13
+ const id =
14
+ typeof result.id === "string"
15
+ ? result.id
16
+ : (result.fields as Record<string, { value?: string }> | undefined)?.Id?.value;
17
+ if (!id) throw new Error("Create succeeded but no record id returned");
18
+ return id;
19
+ }
20
+
21
+ export interface ContactUsInput {
22
+ FirstName: string;
23
+ LastName: string;
24
+ Email: string;
25
+ Phone?: string;
26
+ Subject?: string;
27
+ Message: string;
28
+ }
29
+
30
+ /**
31
+ * Creates a Lead from the Contact Us form. Uses Company for subject, Description for message.
32
+ */
33
+ export async function createContactUsLead(input: ContactUsInput): Promise<{ id: string }> {
34
+ const fields: Record<string, unknown> = {
35
+ FirstName: input.FirstName.trim(),
36
+ LastName: input.LastName.trim(),
37
+ Email: input.Email.trim(),
38
+ Company: (input.Subject ?? "Contact Us").trim() || "Contact Us",
39
+ Description: input.Message.trim() || "",
40
+ LeadSource: LEAD_SOURCE_WEBSITE,
41
+ };
42
+ if (input.Phone?.trim()) {
43
+ fields.Phone = input.Phone.trim();
44
+ }
45
+ const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
46
+ string,
47
+ unknown
48
+ >;
49
+ return { id: getRecordIdFromResponse(result) };
50
+ }
51
+
52
+ /**
53
+ * Creates a Lead for newsletter/email signup. LastName and Company set to placeholders so Lead is valid.
54
+ */
55
+ export async function createNewsletterLead(email: string): Promise<{ id: string }> {
56
+ const trimmed = email.trim();
57
+ if (!trimmed) throw new Error("Email is required");
58
+ const fields: Record<string, unknown> = {
59
+ LastName: "Newsletter Subscriber",
60
+ Company: "Website",
61
+ Email: trimmed,
62
+ LeadSource: LEAD_SOURCE_NEWSLETTER,
63
+ };
64
+ const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
65
+ string,
66
+ unknown
67
+ >;
68
+ return { id: getRecordIdFromResponse(result) };
69
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Maintenance_Request__c: list via GraphQL, create via createRecord.
3
+ */
4
+ import { createRecord } from "@salesforce/webapp-experimental/api";
5
+ import { executeGraphQL } from "./graphqlClient.js";
6
+
7
+ const OBJECT_API_NAME = "Maintenance_Request__c";
8
+
9
+ export interface MaintenanceRequestSummary {
10
+ id: string;
11
+ name: string | null;
12
+ title: string | null;
13
+ description: string | null;
14
+ type: string | null;
15
+ priority: string | null;
16
+ status: string | null;
17
+ dateRequested: string | null;
18
+ }
19
+
20
+ function pick<T>(obj: T, key: keyof T): unknown {
21
+ const v = obj[key];
22
+ if (v != null && typeof v === "object" && "value" in v) {
23
+ return (v as { value?: unknown }).value ?? null;
24
+ }
25
+ return v ?? null;
26
+ }
27
+
28
+ function str(x: unknown): string | null {
29
+ if (x == null) return null;
30
+ return String(x);
31
+ }
32
+
33
+ type Node = {
34
+ Id: string;
35
+ ApiName?: string | null;
36
+ Name?: { value?: unknown; displayValue?: string | null } | null;
37
+ Title__c?: { value?: unknown; displayValue?: string | null } | null;
38
+ Description__c?: { value?: unknown; displayValue?: string | null } | null;
39
+ Type__c?: { value?: unknown; displayValue?: string | null } | null;
40
+ Priority__c?: { value?: unknown; displayValue?: string | null } | null;
41
+ Status__c?: { value?: unknown; displayValue?: string | null } | null;
42
+ Date_Requested__c?: { value?: unknown; displayValue?: string | null } | null;
43
+ };
44
+
45
+ function nodeToSummary(node: Node): MaintenanceRequestSummary {
46
+ return {
47
+ id: node.Id,
48
+ name:
49
+ str(pick(node, "Name")) ?? str((node.Name as { displayValue?: string | null })?.displayValue),
50
+ title: str(pick(node, "Title__c")),
51
+ description: str(pick(node, "Description__c")),
52
+ type: str(pick(node, "Type__c")),
53
+ priority: str(pick(node, "Priority__c")),
54
+ status: str(pick(node, "Status__c")),
55
+ dateRequested: str(pick(node, "Date_Requested__c")),
56
+ };
57
+ }
58
+
59
+ const MAINTENANCE_REQUESTS_QUERY = /* GraphQL */ `
60
+ query MaintenanceRequests($first: Int!, $after: String) {
61
+ uiapi {
62
+ query {
63
+ Maintenance_Request__c(first: $first, after: $after) {
64
+ edges {
65
+ node {
66
+ Id
67
+ ApiName
68
+ Name {
69
+ value
70
+ displayValue
71
+ }
72
+ Title__c {
73
+ value
74
+ displayValue
75
+ }
76
+ Description__c {
77
+ value
78
+ displayValue
79
+ }
80
+ Type__c {
81
+ value
82
+ displayValue
83
+ }
84
+ Priority__c {
85
+ value
86
+ displayValue
87
+ }
88
+ Status__c {
89
+ value
90
+ displayValue
91
+ }
92
+ Date_Requested__c {
93
+ value
94
+ displayValue
95
+ }
96
+ }
97
+ cursor
98
+ }
99
+ pageInfo {
100
+ hasNextPage
101
+ endCursor
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ `;
108
+
109
+ interface MaintenanceRequestGraphQLResponse {
110
+ uiapi?: {
111
+ query?: {
112
+ Maintenance_Request__c?: {
113
+ edges?: ({ node?: Node | null } | null)[] | null;
114
+ } | null;
115
+ } | null;
116
+ } | null;
117
+ }
118
+
119
+ export async function queryMaintenanceRequests(
120
+ first: number = 50,
121
+ after: string | null = null,
122
+ ): Promise<MaintenanceRequestSummary[]> {
123
+ const response = await executeGraphQL<MaintenanceRequestGraphQLResponse>(
124
+ MAINTENANCE_REQUESTS_QUERY,
125
+ { first, after: after ?? null } as Record<string, unknown>,
126
+ );
127
+ const edges = response.uiapi?.query?.Maintenance_Request__c?.edges ?? [];
128
+ const list: MaintenanceRequestSummary[] = [];
129
+ for (const e of edges) {
130
+ if (e?.node) list.push(nodeToSummary(e.node));
131
+ }
132
+ // Sort by date requested descending (newest first)
133
+ list.sort((a, b) => {
134
+ const da = a.dateRequested ?? "";
135
+ const db = b.dateRequested ?? "";
136
+ return db.localeCompare(da);
137
+ });
138
+ return list;
139
+ }
140
+
141
+ export interface CreateMaintenanceRequestInput {
142
+ Title__c: string;
143
+ Description__c?: string | null;
144
+ Type__c?: string | null;
145
+ Priority__c?: string;
146
+ Status__c?: string;
147
+ Date_Requested__c?: string | null;
148
+ }
149
+
150
+ function getRecordIdFromResponse(result: Record<string, unknown>): string {
151
+ const id =
152
+ typeof result.id === "string"
153
+ ? result.id
154
+ : (result.fields as Record<string, { value?: string }> | undefined)?.Id?.value;
155
+ if (!id) throw new Error("Create succeeded but no record id returned");
156
+ return id;
157
+ }
158
+
159
+ export async function createMaintenanceRequest(
160
+ input: CreateMaintenanceRequestInput,
161
+ ): Promise<{ id: string }> {
162
+ const title = input.Title__c?.trim();
163
+ if (!title) throw new Error("Title is required");
164
+ const fields: Record<string, unknown> = {
165
+ Title__c: title,
166
+ Priority__c: input.Priority__c?.trim() || "Standard",
167
+ Status__c: input.Status__c?.trim() || "New",
168
+ };
169
+ if (input.Description__c?.trim()) fields.Description__c = input.Description__c.trim();
170
+ if (input.Type__c?.trim()) fields.Type__c = input.Type__c.trim();
171
+ if (input.Date_Requested__c?.trim()) fields.Date_Requested__c = input.Date_Requested__c.trim();
172
+ const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
173
+ string,
174
+ unknown
175
+ >;
176
+ return { id: getRecordIdFromResponse(result) };
177
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Record detail service: layout (REST), object metadata (GraphQL), single record (GraphQL).
3
+ *
4
+ * getRecordDetail orchestrates layout + objectInfoBatch + getRecordByIdGraphQL for the detail page.
5
+ * Layout is still REST (uiApiClient); record and object info are GraphQL-backed.
6
+ *
7
+ * @module api/objectDetailService
8
+ */
9
+
10
+ import { uiApiClient } from "@salesforce/webapp-experimental/api";
11
+ import type { LayoutResponse } from "../types/recordDetail/recordDetail";
12
+ import { LayoutResponseSchema } from "../types/recordDetail/recordDetail";
13
+ import { fetchAndValidate, safeEncodePath } from "../utils/apiUtils";
14
+ import { objectInfoService } from "../api/objectInfoService";
15
+ import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
16
+ import { getRecordByIdGraphQL, type GraphQLRecordNode } from "../api/recordListGraphQLService";
17
+ import type { Column } from "../types/search/searchResults";
18
+ import { calculateFieldsToFetch } from "../utils/recordUtils";
19
+
20
+ /** Fallback when record type is unknown. Prefer recordTypeId from the record (e.g. from search or record response) when available. */
21
+ const DEFAULT_RECORD_TYPE_ID = "012000000000000AAA";
22
+
23
+ /**
24
+ * Returns field API names to request for a record from the given layout and object metadata.
25
+ * Used to derive GraphQL columns from layout (detail view). Delegates to recordUtils.calculateFieldsToFetch.
26
+ *
27
+ * @param objectMetadata - Object info (fields, relationshipName, etc.).
28
+ * @param layout - Layout response (sections, layoutItems, layoutComponents).
29
+ * @returns Array of field API names (e.g. ["Name", "OwnerId", "Owner", "CreatedDate"]).
30
+ */
31
+ export function extractFieldsFromLayout(
32
+ objectMetadata: ObjectInfoResult,
33
+ layout: LayoutResponse,
34
+ ): string[] {
35
+ const [optionalFields] = calculateFieldsToFetch(objectMetadata, layout, false);
36
+ return optionalFields;
37
+ }
38
+
39
+ /**
40
+ * Fetches the Full/View layout for an object (REST). Used by detail view to render sections/rows/items.
41
+ *
42
+ * @param objectApiName - Object API name.
43
+ * @param recordTypeId - Record type Id (default master).
44
+ * @param signal - Optional abort signal.
45
+ */
46
+ export async function getLayout(
47
+ objectApiName: string,
48
+ recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
49
+ signal?: AbortSignal,
50
+ ): Promise<LayoutResponse> {
51
+ const params = new URLSearchParams({
52
+ layoutType: "Full",
53
+ mode: "View",
54
+ recordTypeId,
55
+ });
56
+ return fetchAndValidate(
57
+ (abortSignal) =>
58
+ uiApiClient.get(`/layout/${safeEncodePath(objectApiName)}?${params.toString()}`, {
59
+ signal: abortSignal,
60
+ }),
61
+ {
62
+ schema: LayoutResponseSchema,
63
+ errorContext: `layout for ${objectApiName}`,
64
+ signal,
65
+ },
66
+ );
67
+ }
68
+
69
+ export interface RecordDetailResult {
70
+ layout: LayoutResponse;
71
+ record: GraphQLRecordNode;
72
+ objectMetadata: ObjectInfoResult;
73
+ }
74
+
75
+ /**
76
+ * Converts layout-derived optionalFields (field API names) to Column[] for GraphQL node selection.
77
+ * Uses unqualified names (no entity prefix) so the GraphQL query matches UI API shape.
78
+ * Other Column fields (label, searchable, sortable) are only required by the type; GraphQL selection uses fieldApiName only.
79
+ */
80
+ function optionalFieldsToColumns(optionalFields: string[]): Column[] {
81
+ return optionalFields.map((fieldApiName) => ({
82
+ fieldApiName,
83
+ label: "",
84
+ searchable: false,
85
+ sortable: false,
86
+ }));
87
+ }
88
+
89
+ /**
90
+ * Fetches everything needed for the detail page: layout (REST), object metadata (GraphQL), single record (GraphQL).
91
+ * Layout drives which fields are requested; getRecordByIdGraphQL fetches that field set by Id.
92
+ *
93
+ * @param objectApiName - Object API name.
94
+ * @param recordId - Record Id.
95
+ * @param recordTypeId - Record type (default master).
96
+ * @param signal - Optional abort signal.
97
+ * @returns { layout, record, objectMetadata } for DetailForm / UiApiDetailForm.
98
+ */
99
+ export async function getRecordDetail(
100
+ objectApiName: string,
101
+ recordId: string,
102
+ recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
103
+ signal?: AbortSignal,
104
+ ): Promise<RecordDetailResult> {
105
+ const layout = await getLayout(objectApiName, recordTypeId, signal);
106
+ const objectMetadata = await objectInfoService.getObjectInfoBatch(objectApiName, signal);
107
+ const firstResult = objectMetadata?.results?.[0]?.result;
108
+ if (!firstResult) {
109
+ throw new Error(`Object metadata not found for ${objectApiName}`);
110
+ }
111
+ // Layout-driven optionalFields (fields shown on the detail layout), not list columns
112
+ const [optionalFields] = calculateFieldsToFetch(firstResult, layout, false);
113
+ const columns = optionalFieldsToColumns(optionalFields);
114
+ const record = await getRecordByIdGraphQL(objectApiName, recordId, columns);
115
+ if (!record) {
116
+ throw new Error(`Record not found: ${recordId}`);
117
+ }
118
+ return { layout, record, objectMetadata: firstResult };
119
+ }
120
+
121
+ export const objectDetailService = {
122
+ extractFieldsFromLayout,
123
+ getLayout,
124
+ getRecordDetail,
125
+ };