@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.
- package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/SKILL.md +5 -3
- package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/header-footer.md +8 -0
- package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/page.md +8 -7
- package/dist/.a4drules/skills/webapp-ui-ux/SKILL.md +11 -8
- package/dist/.a4drules/webapp-react.md +54 -0
- package/dist/CHANGELOG.md +16 -0
- package/dist/README.md +24 -0
- package/dist/force-app/main/default/data/Property_Image__c.json +1 -1
- package/dist/force-app/main/default/data/Property_Listing__c.json +1 -1
- package/dist/force-app/main/default/data/prepare-import-unique-fields.js +85 -0
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +0 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/index.html +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +9 -9
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +296 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +12 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +50 -38
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +50 -102
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +211 -43
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/userApi.ts +43 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +9 -208
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/appliances.svg +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/electrical.svg +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/hvac.svg +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/pest.svg +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/plumbing.svg +7 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/zen-logo.svg +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceRequestIcon.tsx +46 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/NavMenu.tsx +53 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +55 -58
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +93 -11
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertySearchFilters.tsx +315 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/StatusBadge.tsx +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/TopBar.tsx +107 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +64 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +14 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +54 -11
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +42 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +10 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +64 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +19 -9
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +79 -100
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/NotFound.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +62 -47
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +3 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +230 -34
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +10 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +64 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +30 -5
- package/dist/package.json +1 -1
- package/dist/setup-cli.mjs +271 -0
- package/package.json +1 -1
- /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 {
|
|
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 =
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
186
|
-
? String(node.
|
|
187
|
-
: (node.
|
|
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.
|
|
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 =
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 =
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 =
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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 {
|
|
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 "
|
|
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? }.
|
|
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
|
-
|
|
25
|
-
|
|
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)
|
|
78
|
-
* Uses Property_Listing__c_Filter
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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<
|
|
289
|
+
const response = await executeGraphQL<PropertyListingsQuery, PropertyListingsQueryVariables>(
|
|
153
290
|
PROPERTY_LISTINGS_QUERY,
|
|
154
|
-
variables
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|