@salesforce/webapp-template-app-react-sample-b2x-experimental 1.84.0 → 1.85.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 (57) hide show
  1. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/SKILL.md +5 -3
  2. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/header-footer.md +8 -0
  3. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/page.md +8 -7
  4. package/dist/.a4drules/skills/webapp-ui-ux/SKILL.md +11 -8
  5. package/dist/.a4drules/webapp-react.md +54 -0
  6. package/dist/CHANGELOG.md +16 -0
  7. package/dist/README.md +24 -0
  8. package/dist/force-app/main/default/data/Property_Image__c.json +1 -1
  9. package/dist/force-app/main/default/data/Property_Listing__c.json +1 -1
  10. package/dist/force-app/main/default/data/prepare-import-unique-fields.js +85 -0
  11. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +0 -7
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/index.html +6 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +9 -9
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +296 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +12 -7
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +50 -38
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +50 -102
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +211 -43
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/userApi.ts +43 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +9 -208
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/appliances.svg +13 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/electrical.svg +39 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/hvac.svg +78 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/pest.svg +5 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/plumbing.svg +7 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/zen-logo.svg +5 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceRequestIcon.tsx +46 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/NavMenu.tsx +53 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +55 -58
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +93 -11
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertySearchFilters.tsx +315 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/StatusBadge.tsx +36 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/TopBar.tsx +107 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +55 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +64 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +14 -5
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +54 -11
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +1 -1
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +42 -39
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +10 -10
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +64 -91
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +1 -1
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +19 -9
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +79 -100
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/NotFound.tsx +1 -1
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +62 -47
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +3 -3
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +230 -34
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +10 -1
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +64 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +30 -5
  54. package/dist/package.json +1 -1
  55. package/dist/setup-cli.mjs +271 -0
  56. package/package.json +1 -1
  57. /package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/component.md +0 -0
@@ -2,10 +2,23 @@
2
2
  * GraphQL queries for Property_Listing__c detail and related Property__c data:
3
3
  * Property_Image__c, Property_Cost__c, Property_Feature__c.
4
4
  */
5
- import { executeGraphQL } from "./graphqlClient";
5
+ import { gql } from "@salesforce/sdk-data";
6
+ import type {
7
+ ListingByIdQuery,
8
+ ListingByIdQueryVariables,
9
+ PropertyByIdQuery,
10
+ PropertyByIdQueryVariables,
11
+ PropertyImagesQuery,
12
+ PropertyImagesQueryVariables,
13
+ PropertyCostsQuery,
14
+ PropertyCostsQueryVariables,
15
+ PropertyFeaturesQuery,
16
+ PropertyFeaturesQueryVariables,
17
+ } from "@/api/graphql-operations-types.js";
18
+ import { executeGraphQL } from "@/api/graphqlClient.js";
6
19
 
7
20
  // ---- Listing by Id ----
8
- const LISTING_QUERY = /* GraphQL */ `
21
+ const LISTING_QUERY = gql`
9
22
  query ListingById($listingId: ID!) {
10
23
  uiapi {
11
24
  query {
@@ -46,23 +59,11 @@ export interface ListingDetail {
46
59
  }
47
60
 
48
61
  export async function fetchListingById(listingId: string): Promise<ListingDetail | null> {
49
- const res = await executeGraphQL<{
50
- uiapi?: {
51
- query?: {
52
- Property_Listing__c?: {
53
- edges?: Array<{
54
- node?: {
55
- Id: string;
56
- Name?: { value?: string | null; displayValue?: string | null } | null;
57
- Listing_Price__c?: { value?: unknown; displayValue?: string | null } | null;
58
- Listing_Status__c?: { value?: string | null; displayValue?: string | null } | null;
59
- Property__c?: { value?: string | null; displayValue?: string | null } | null;
60
- } | null;
61
- }> | null;
62
- } | null;
63
- } | null;
64
- } | null;
65
- }>(LISTING_QUERY, { listingId });
62
+ const variables: ListingByIdQueryVariables = { listingId };
63
+ const res = await executeGraphQL<ListingByIdQuery, ListingByIdQueryVariables>(
64
+ LISTING_QUERY,
65
+ variables,
66
+ );
66
67
  const node = res.uiapi?.query?.Property_Listing__c?.edges?.[0]?.node;
67
68
  if (!node) return null;
68
69
  const prop = node.Property__c;
@@ -84,7 +85,7 @@ export async function fetchListingById(listingId: string): Promise<ListingDetail
84
85
  }
85
86
 
86
87
  // ---- Property by Id ----
87
- const PROPERTY_QUERY = /* GraphQL */ `
88
+ const PROPERTY_QUERY = gql`
88
89
  query PropertyById($propertyId: ID!) {
89
90
  uiapi {
90
91
  query {
@@ -100,7 +101,7 @@ const PROPERTY_QUERY = /* GraphQL */ `
100
101
  value
101
102
  displayValue
102
103
  }
103
- Property_Type__c {
104
+ Type__c {
104
105
  value
105
106
  displayValue
106
107
  }
@@ -116,7 +117,7 @@ const PROPERTY_QUERY = /* GraphQL */ `
116
117
  value
117
118
  displayValue
118
119
  }
119
- Square_Footage__c {
120
+ Sq_Ft__c {
120
121
  value
121
122
  displayValue
122
123
  }
@@ -145,27 +146,11 @@ export interface PropertyDetail {
145
146
  }
146
147
 
147
148
  export async function fetchPropertyById(propertyId: string): Promise<PropertyDetail | null> {
148
- const res = await executeGraphQL<{
149
- uiapi?: {
150
- query?: {
151
- Property__c?: {
152
- edges?: Array<{
153
- node?: {
154
- Id: string;
155
- Name?: { value?: string | null; displayValue?: string | null } | null;
156
- Address__c?: { value?: string | null; displayValue?: string | null } | null;
157
- Property_Type__c?: { value?: string | null; displayValue?: string | null } | null;
158
- Monthly_Rent__c?: { value?: unknown; displayValue?: string | null } | null;
159
- Bedrooms__c?: { value?: unknown; displayValue?: string | null } | null;
160
- Bathrooms__c?: { value?: unknown; displayValue?: string | null } | null;
161
- Square_Footage__c?: { value?: unknown; displayValue?: string | null } | null;
162
- Description__c?: { value?: string | null; displayValue?: string | null } | null;
163
- } | null;
164
- }> | null;
165
- } | null;
166
- } | null;
167
- } | null;
168
- }>(PROPERTY_QUERY, { propertyId });
149
+ const variables: PropertyByIdQueryVariables = { propertyId };
150
+ const res = await executeGraphQL<PropertyByIdQuery, PropertyByIdQueryVariables>(
151
+ PROPERTY_QUERY,
152
+ variables,
153
+ );
169
154
  const node = res.uiapi?.query?.Property__c?.edges?.[0]?.node;
170
155
  if (!node) return null;
171
156
  const v = (f: { value?: unknown; displayValue?: string | null } | null | undefined) =>
@@ -182,13 +167,13 @@ export async function fetchPropertyById(propertyId: string): Promise<PropertyDet
182
167
  ? String(node.Address__c.value)
183
168
  : (node.Address__c?.displayValue ?? null),
184
169
  propertyType:
185
- node.Property_Type__c?.value != null
186
- ? String(node.Property_Type__c.value)
187
- : (node.Property_Type__c?.displayValue ?? null),
170
+ node.Type__c?.value != null
171
+ ? String(node.Type__c.value)
172
+ : (node.Type__c?.displayValue ?? null),
188
173
  monthlyRent: v(node.Monthly_Rent__c),
189
174
  bedrooms: v(node.Bedrooms__c),
190
175
  bathrooms: v(node.Bathrooms__c),
191
- squareFootage: v(node.Square_Footage__c),
176
+ squareFootage: v(node.Sq_Ft__c),
192
177
  description:
193
178
  node.Description__c?.value != null
194
179
  ? String(node.Description__c.value)
@@ -211,7 +196,7 @@ export async function fetchPropertyAddresses(
211
196
  }
212
197
 
213
198
  // ---- Property Images by Property Id ----
214
- const IMAGES_QUERY = /* GraphQL */ `
199
+ const IMAGES_QUERY = gql`
215
200
  query PropertyImages($propertyId: ID!) {
216
201
  uiapi {
217
202
  query {
@@ -257,24 +242,11 @@ export interface PropertyImageRecord {
257
242
  }
258
243
 
259
244
  export async function fetchImagesByPropertyId(propertyId: string): Promise<PropertyImageRecord[]> {
260
- const res = await executeGraphQL<{
261
- uiapi?: {
262
- query?: {
263
- Property_Image__c?: {
264
- edges?: Array<{
265
- node?: {
266
- Id: string;
267
- Name?: { value?: string | null; displayValue?: string | null } | null;
268
- Image_URL__c?: { value?: string | null; displayValue?: string | null } | null;
269
- Image_Type__c?: { value?: string | null; displayValue?: string | null } | null;
270
- Display_Order__c?: { value?: unknown; displayValue?: string | null } | null;
271
- Alt_Text__c?: { value?: string | null; displayValue?: string | null } | null;
272
- } | null;
273
- }> | null;
274
- } | null;
275
- } | null;
276
- } | null;
277
- }>(IMAGES_QUERY, { propertyId });
245
+ const variables: PropertyImagesQueryVariables = { propertyId };
246
+ const res = await executeGraphQL<PropertyImagesQuery, PropertyImagesQueryVariables>(
247
+ IMAGES_QUERY,
248
+ variables,
249
+ );
278
250
  const edges = res.uiapi?.query?.Property_Image__c?.edges ?? [];
279
251
  const list: PropertyImageRecord[] = [];
280
252
  for (const e of edges) {
@@ -304,7 +276,7 @@ export async function fetchImagesByPropertyId(propertyId: string): Promise<Prope
304
276
  }
305
277
 
306
278
  // ---- Property Costs by Property Id ----
307
- const COSTS_QUERY = /* GraphQL */ `
279
+ const COSTS_QUERY = gql`
308
280
  query PropertyCosts($propertyId: ID!) {
309
281
  uiapi {
310
282
  query {
@@ -350,24 +322,11 @@ export interface PropertyCostRecord {
350
322
  }
351
323
 
352
324
  export async function fetchCostsByPropertyId(propertyId: string): Promise<PropertyCostRecord[]> {
353
- const res = await executeGraphQL<{
354
- uiapi?: {
355
- query?: {
356
- Property_Cost__c?: {
357
- edges?: Array<{
358
- node?: {
359
- Id: string;
360
- Cost_Category__c?: { value?: string | null; displayValue?: string | null } | null;
361
- Cost_Amount__c?: { value?: unknown; displayValue?: string | null } | null;
362
- Cost_Date__c?: { value?: string | null; displayValue?: string | null } | null;
363
- Description__c?: { value?: string | null; displayValue?: string | null } | null;
364
- Vendor__c?: { value?: string | null; displayValue?: string | null } | null;
365
- } | null;
366
- }> | null;
367
- } | null;
368
- } | null;
369
- } | null;
370
- }>(COSTS_QUERY, { propertyId });
325
+ const variables: PropertyCostsQueryVariables = { propertyId };
326
+ const res = await executeGraphQL<PropertyCostsQuery, PropertyCostsQueryVariables>(
327
+ COSTS_QUERY,
328
+ variables,
329
+ );
371
330
  const edges = res.uiapi?.query?.Property_Cost__c?.edges ?? [];
372
331
  const list: PropertyCostRecord[] = [];
373
332
  for (const e of edges) {
@@ -404,7 +363,7 @@ export async function fetchCostsByPropertyId(propertyId: string): Promise<Proper
404
363
  }
405
364
 
406
365
  // ---- Property Features by Property Id ----
407
- const FEATURES_QUERY = /* GraphQL */ `
366
+ const FEATURES_QUERY = gql`
408
367
  query PropertyFeatures($propertyId: ID!) {
409
368
  uiapi {
410
369
  query {
@@ -442,22 +401,11 @@ export interface PropertyFeatureRecord {
442
401
  export async function fetchFeaturesByPropertyId(
443
402
  propertyId: string,
444
403
  ): Promise<PropertyFeatureRecord[]> {
445
- const res = await executeGraphQL<{
446
- uiapi?: {
447
- query?: {
448
- Property_Feature__c?: {
449
- edges?: Array<{
450
- node?: {
451
- Id: string;
452
- Name?: { value?: string | null; displayValue?: string | null } | null;
453
- Feature_Category__c?: { value?: string | null; displayValue?: string | null } | null;
454
- Description__c?: { value?: string | null; displayValue?: string | null } | null;
455
- } | null;
456
- }> | null;
457
- } | null;
458
- } | null;
459
- } | null;
460
- }>(FEATURES_QUERY, { propertyId });
404
+ const variables: PropertyFeaturesQueryVariables = { propertyId };
405
+ const res = await executeGraphQL<PropertyFeaturesQuery, PropertyFeaturesQueryVariables>(
406
+ FEATURES_QUERY,
407
+ variables,
408
+ );
461
409
  const edges = res.uiapi?.query?.Property_Feature__c?.edges ?? [];
462
410
  const list: PropertyFeatureRecord[] = [];
463
411
  for (const e of edges) {
@@ -2,16 +2,22 @@
2
2
  * Property Listing search via Salesforce GraphQL.
3
3
  * Replaces REST keyword search with uiapi.query.Property_Listing__c.
4
4
  */
5
- import { executeGraphQL } from "./graphqlClient";
5
+ import { gql } from "@salesforce/sdk-data";
6
+ import type {
7
+ PropertyListingsQuery,
8
+ PropertyListingsQueryVariables,
9
+ } from "@/api/graphql-operations-types.js";
10
+ import { ResultOrder } from "@/api/graphql-operations-types.js";
11
+ import { executeGraphQL } from "@/api/graphqlClient.js";
6
12
  import type {
7
13
  SearchResultRecord,
8
14
  SearchResultRecordData,
9
15
  FieldValue,
10
- } from "../features/global-search/types/search/searchResults";
16
+ } from "@/features/global-search/types/search/searchResults.js";
11
17
 
12
18
  const OBJECT_API_NAME = "Property_Listing__c";
13
19
 
14
- /** GraphQL node shape: fields are { value?, displayValue? }. Only fields that exist in the org's UI API schema are queried. */
20
+ /** GraphQL node shape: fields are { value?, displayValue? }. Property__r for property name and location. */
15
21
  type PropertyListingNode = {
16
22
  Id: string;
17
23
  ApiName?: string | null;
@@ -19,24 +25,12 @@ type PropertyListingNode = {
19
25
  Listing_Price__c?: { value?: number | null; displayValue?: string | null } | null;
20
26
  Listing_Status__c?: { value?: string | null; displayValue?: string | null } | null;
21
27
  Property__c?: { value?: string | null; displayValue?: string | null } | null;
22
- };
23
-
24
- interface PropertyListingGraphQLResponse {
25
- uiapi?: {
26
- query?: {
27
- Property_Listing__c?: {
28
- edges?: ({ node?: PropertyListingNode | null; cursor?: string | null } | null)[] | null;
29
- pageInfo?: {
30
- hasNextPage?: boolean | null;
31
- hasPreviousPage?: boolean | null;
32
- startCursor?: string | null;
33
- endCursor?: string | null;
34
- } | null;
35
- totalCount?: number | null;
36
- } | null;
37
- } | null;
28
+ Property__r?: {
29
+ Name?: { value?: string | null; displayValue?: string | null } | null;
30
+ Address__c?: { value?: string | null; displayValue?: string | null } | null;
31
+ Bedrooms__c?: { value?: number | null; displayValue?: string | null } | null;
38
32
  } | null;
39
- }
33
+ };
40
34
 
41
35
  function nodeToFieldValue(
42
36
  fieldObj: { value?: unknown; displayValue?: string | null } | null | undefined,
@@ -64,6 +58,31 @@ function nodeToSearchResultRecordData(node: PropertyListingNode): SearchResultRe
64
58
  fields[key] = nodeToFieldValue(raw as { value?: unknown; displayValue?: string | null });
65
59
  }
66
60
  }
61
+ // Flatten property name and address for display (read-only fields for search results)
62
+ const prop = node.Property__r;
63
+ if (prop?.Name != null && typeof prop.Name === "object" && "value" in prop.Name) {
64
+ fields["Property__r.Name"] = nodeToFieldValue(
65
+ prop.Name as { value?: unknown; displayValue?: string | null },
66
+ );
67
+ }
68
+ if (
69
+ prop?.Address__c != null &&
70
+ typeof prop.Address__c === "object" &&
71
+ "value" in prop.Address__c
72
+ ) {
73
+ fields["Property__r.Address__c"] = nodeToFieldValue(
74
+ prop.Address__c as { value?: unknown; displayValue?: string | null },
75
+ );
76
+ }
77
+ if (
78
+ prop?.Bedrooms__c != null &&
79
+ typeof prop.Bedrooms__c === "object" &&
80
+ "value" in prop.Bedrooms__c
81
+ ) {
82
+ fields["Property__r.Bedrooms__c"] = nodeToFieldValue(
83
+ prop.Bedrooms__c as { value?: unknown; displayValue?: string | null },
84
+ );
85
+ }
67
86
  return {
68
87
  id: node.Id,
69
88
  apiName: typeof node.ApiName === "string" ? node.ApiName : OBJECT_API_NAME,
@@ -74,14 +93,18 @@ function nodeToSearchResultRecordData(node: PropertyListingNode): SearchResultRe
74
93
  };
75
94
  }
76
95
 
77
- /** Query with optional search term (Name like) and pagination. When searchTerm is empty, no where clause = show all.
78
- * Uses Property_Listing__c_Filter (object API name + _Filter). No orderBy to avoid org schema mismatches.
79
- * ApiName is a leaf (String!); only fields that exist in the org's UI API schema are selected. */
80
- const PROPERTY_LISTINGS_QUERY = /* GraphQL */ `
81
- query PropertyListings($where: Property_Listing__c_Filter, $first: Int!, $after: String) {
96
+ /** Query with optional search term (Name like), pagination, and orderBy.
97
+ * Uses Property_Listing__c_Filter. orderBy is optional; omit if org schema does not support it. */
98
+ const PROPERTY_LISTINGS_QUERY = gql`
99
+ query PropertyListings(
100
+ $where: Property_Listing__c_Filter
101
+ $first: Int!
102
+ $after: String
103
+ $orderBy: Property_Listing__c_OrderBy
104
+ ) {
82
105
  uiapi {
83
106
  query {
84
- Property_Listing__c(where: $where, first: $first, after: $after) {
107
+ Property_Listing__c(where: $where, first: $first, after: $after, orderBy: $orderBy) {
85
108
  edges {
86
109
  node {
87
110
  Id
@@ -102,6 +125,20 @@ const PROPERTY_LISTINGS_QUERY = /* GraphQL */ `
102
125
  value
103
126
  displayValue
104
127
  }
128
+ Property__r {
129
+ Name {
130
+ value
131
+ displayValue
132
+ }
133
+ Address__c {
134
+ value
135
+ displayValue
136
+ }
137
+ Bedrooms__c {
138
+ value
139
+ displayValue
140
+ }
141
+ }
105
142
  }
106
143
  cursor
107
144
  }
@@ -126,45 +163,143 @@ export interface PropertyListingGraphQLResult {
126
163
  totalCount: number | null;
127
164
  }
128
165
 
166
+ /** SortBy values match PropertySearchFilters SortBy (price_asc, price_desc, beds_asc, beds_desc). */
167
+ export interface PropertyListingFilters {
168
+ priceMin?: number;
169
+ priceMax?: number;
170
+ bedroomsMin?: number;
171
+ bedroomsMax?: number;
172
+ sortBy?: "price_asc" | "price_desc" | "beds_asc" | "beds_desc" | null;
173
+ }
174
+
175
+ /** Where condition for Property_Listing__c_Filter (matches graphql-operations-types.ts). */
176
+ type PropertyListingsWhereCondition = {
177
+ Name?: { like: string };
178
+ Listing_Status__c?: { like: string };
179
+ Listing_Price__c?: { gte?: number; lte?: number };
180
+ Property__r?: {
181
+ Name?: { like: string };
182
+ Address__c?: { like: string };
183
+ Bedrooms__c?: { gte?: number; lte?: number };
184
+ };
185
+ };
186
+
187
+ /** Text search: name or Property__r name/address. Returns { or: [...] } or undefined. */
188
+ function buildTextWhere(
189
+ searchTerm: string,
190
+ ): NonNullable<PropertyListingsQueryVariables["where"]> | undefined {
191
+ const term = searchTerm.trim();
192
+ if (term.length === 0) return undefined;
193
+ const pattern = `%${term}%`;
194
+ return {
195
+ or: [
196
+ { Name: { like: pattern } },
197
+ { Property__r: { Name: { like: pattern } } },
198
+ { Property__r: { Address__c: { like: pattern } } },
199
+ ],
200
+ };
201
+ }
202
+
203
+ /** Element type allowed inside { and: [...] } (no nested and). */
204
+ type AndElement = PropertyListingsWhereCondition | { or: PropertyListingsWhereCondition[] };
205
+
206
+ /** Combines text search + price range + bedroom range (min/max) into one where (and of conditions). */
207
+ function buildWhere(
208
+ searchTerm: string,
209
+ filters: PropertyListingFilters | undefined,
210
+ ): PropertyListingsQueryVariables["where"] {
211
+ const conditions: AndElement[] = [];
212
+ const textWhere = buildTextWhere(searchTerm);
213
+ if (textWhere) conditions.push(textWhere as AndElement);
214
+ const priceMin =
215
+ filters?.priceMin != null && Number.isFinite(filters.priceMin) ? filters.priceMin : undefined;
216
+ const priceMax =
217
+ filters?.priceMax != null && Number.isFinite(filters.priceMax) ? filters.priceMax : undefined;
218
+ if (priceMin != null || priceMax != null) {
219
+ conditions.push({
220
+ Listing_Price__c: {
221
+ ...(priceMin != null && { gte: priceMin }),
222
+ ...(priceMax != null && { lte: priceMax }),
223
+ },
224
+ });
225
+ }
226
+ const bedroomsMin =
227
+ filters?.bedroomsMin != null && Number.isFinite(filters.bedroomsMin) && filters.bedroomsMin >= 0
228
+ ? filters.bedroomsMin
229
+ : undefined;
230
+ const bedroomsMax =
231
+ filters?.bedroomsMax != null && Number.isFinite(filters.bedroomsMax) && filters.bedroomsMax >= 0
232
+ ? filters.bedroomsMax
233
+ : undefined;
234
+ if (bedroomsMin != null || bedroomsMax != null) {
235
+ conditions.push({
236
+ Property__r: {
237
+ Bedrooms__c: {
238
+ ...(bedroomsMin != null && { gte: bedroomsMin }),
239
+ ...(bedroomsMax != null && { lte: bedroomsMax }),
240
+ },
241
+ },
242
+ });
243
+ }
244
+ if (conditions.length === 0) return undefined;
245
+ if (conditions.length === 1) return conditions[0];
246
+ return { and: conditions };
247
+ }
248
+
249
+ /** Build orderBy for Property_Listing__c from SortBy. Returns undefined when sortBy is null or not supported. */
250
+ function buildOrderBy(
251
+ sortBy: PropertyListingFilters["sortBy"],
252
+ ): PropertyListingsQueryVariables["orderBy"] {
253
+ if (sortBy == null) return undefined;
254
+ switch (sortBy) {
255
+ case "price_asc":
256
+ return { Listing_Price__c: { order: ResultOrder.Asc } };
257
+ case "price_desc":
258
+ return { Listing_Price__c: { order: ResultOrder.Desc } };
259
+ case "beds_asc":
260
+ return { Property__r: { Bedrooms__c: { order: ResultOrder.Asc } } };
261
+ case "beds_desc":
262
+ return { Property__r: { Bedrooms__c: { order: ResultOrder.Desc } } };
263
+ default:
264
+ return undefined;
265
+ }
266
+ }
267
+
129
268
  /**
130
269
  * Fetch Property_Listing__c records via GraphQL.
131
- * When searchTerm is empty, returns all records (no where clause).
270
+ * Optional text search (name, Property__r name/address), price range (min/max), and bedroom range (min/max).
132
271
  */
133
272
  export async function queryPropertyListingsGraphQL(
134
273
  searchTerm: string,
135
274
  pageSize: number,
136
275
  afterCursor: string | null,
137
276
  _signal?: AbortSignal,
277
+ filters?: PropertyListingFilters,
138
278
  ): Promise<PropertyListingGraphQLResult> {
139
- const where =
140
- searchTerm.trim().length > 0 ? { Name: { like: `%${searchTerm.trim()}%` } } : undefined;
141
-
142
- const variables: {
143
- where?: { Name: { like: string } };
144
- first: number;
145
- after?: string | null;
146
- } = {
279
+ const where = buildWhere(searchTerm, filters);
280
+
281
+ const variables: PropertyListingsQueryVariables = {
147
282
  first: Math.min(Math.max(pageSize, 1), 200),
148
283
  };
149
284
  if (where) variables.where = where;
150
285
  if (afterCursor) variables.after = afterCursor;
286
+ const orderBy = buildOrderBy(filters?.sortBy);
287
+ if (orderBy != null) variables.orderBy = orderBy;
151
288
 
152
- const response = await executeGraphQL<PropertyListingGraphQLResponse>(
289
+ const response = await executeGraphQL<PropertyListingsQuery, PropertyListingsQueryVariables>(
153
290
  PROPERTY_LISTINGS_QUERY,
154
- variables as Record<string, unknown>,
291
+ variables,
155
292
  );
156
293
 
157
294
  const conn = response.uiapi?.query?.Property_Listing__c;
158
295
  const edges = conn?.edges ?? [];
159
296
  const pageInfo = conn?.pageInfo;
160
297
 
161
- type EdgeItem = { node?: PropertyListingNode | null; cursor?: string | null } | null;
162
298
  const records: SearchResultRecord[] = edges
163
- .filter(
164
- (e: EdgeItem): e is { node: PropertyListingNode; cursor?: string | null } => e?.node != null,
165
- )
166
- .map((e: { node: PropertyListingNode; cursor?: string | null }) => {
167
- const data = nodeToSearchResultRecordData(e.node);
299
+ .filter((e) => e != null && e.node != null)
300
+ .map((e) => {
301
+ const node = e!.node!;
302
+ const data = nodeToSearchResultRecordData(node as PropertyListingNode);
168
303
  return {
169
304
  record: data,
170
305
  highlightInfo: { fields: {}, snippet: null },
@@ -181,6 +316,39 @@ export async function queryPropertyListingsGraphQL(
181
316
  };
182
317
  }
183
318
 
319
+ /**
320
+ * Fetch the available price range (min/max) from the first page of listings for the current search.
321
+ * No price or bedroom filters applied. Used to render the filter bar with known bounds.
322
+ */
323
+ export async function queryPropertyListingPriceRange(
324
+ searchTerm: string,
325
+ ): Promise<{ priceMin: number; priceMax: number } | null> {
326
+ const where = buildTextWhere(searchTerm);
327
+ const variables: PropertyListingsQueryVariables = {
328
+ first: 200,
329
+ };
330
+ if (where) variables.where = where;
331
+
332
+ const response = await executeGraphQL<PropertyListingsQuery, PropertyListingsQueryVariables>(
333
+ PROPERTY_LISTINGS_QUERY,
334
+ variables,
335
+ );
336
+
337
+ const edges = response.uiapi?.query?.Property_Listing__c?.edges ?? [];
338
+ const prices: number[] = [];
339
+ for (const edge of edges) {
340
+ const node = edge?.node as PropertyListingNode | null | undefined;
341
+ const raw = node?.Listing_Price__c?.value;
342
+ const num = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
343
+ if (num != null && num >= 0) prices.push(num);
344
+ }
345
+ if (prices.length === 0) return null;
346
+ return {
347
+ priceMin: Math.min(...prices),
348
+ priceMax: Math.max(...prices),
349
+ };
350
+ }
351
+
184
352
  /** Static columns for Property_Listing__c list (only fields exposed in the GraphQL query). */
185
353
  export const PROPERTY_LISTING_COLUMNS = [
186
354
  { fieldApiName: "Name", label: "Name", searchable: true, sortable: true },
@@ -0,0 +1,43 @@
1
+ import { gql } from "@salesforce/sdk-data";
2
+ import type { GetUserInfoQuery } from "@/api/graphql-operations-types.js";
3
+ import { executeGraphQL } from "@/api/graphqlClient.js";
4
+
5
+ const GET_USER_INFO = gql`
6
+ query GetUserInfo {
7
+ uiapi {
8
+ query {
9
+ User(first: 1) {
10
+ edges {
11
+ node {
12
+ Id
13
+ Name {
14
+ value
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
22
+ `;
23
+
24
+ /**
25
+ * Fetches the current user's id and name (for TopBar, etc.).
26
+ * Returns null on error or when no user is returned.
27
+ */
28
+ export async function getUserInfo(): Promise<{ name: string; id: string } | null> {
29
+ try {
30
+ const data = await executeGraphQL<GetUserInfoQuery>(GET_USER_INFO);
31
+ const user = data?.uiapi?.query?.User?.edges?.[0]?.node;
32
+ if (user) {
33
+ return {
34
+ id: user.Id,
35
+ name: user.Name?.value ?? "User",
36
+ };
37
+ }
38
+ return null;
39
+ } catch (error) {
40
+ console.error("Error fetching user info:", error);
41
+ return null;
42
+ }
43
+ }