@salesforce/webapp-template-app-react-sample-b2x-experimental 1.116.7 → 1.116.8

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 (21) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/webapplications/propertyrentalapp/eslint.config.js +13 -2
  3. package/dist/force-app/main/default/webapplications/propertyrentalapp/package.json +3 -3
  4. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyMap.tsx +2 -1
  5. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +1 -1
  6. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/hooks/useCountdownTimer.ts +1 -1
  7. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Profile.tsx +3 -3
  8. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
  9. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +12 -18
  10. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/components/FilterContext.tsx +1 -1
  11. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/hooks/useObjectSearchParams.ts +10 -5
  12. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +4 -3
  13. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingAmenities.ts +4 -3
  14. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +17 -8
  15. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyPrimaryImages.ts +3 -2
  16. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +4 -7
  17. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +1 -1
  18. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx +2 -2
  19. package/dist/package-lock.json +2 -2
  20. package/dist/package.json +1 -1
  21. package/package.json +4 -1
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
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.116.8](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.116.7...v1.116.8) (2026-03-27)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
6
14
  ## [1.116.7](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.116.6...v1.116.7) (2026-03-27)
7
15
 
8
16
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -18,7 +18,12 @@ const schemaExists = existsSync(schemaPath);
18
18
  const config = [
19
19
  // Global ignores
20
20
  {
21
- ignores: ['build/**/*', 'dist/**/*', 'coverage/**/*'],
21
+ ignores: [
22
+ 'build/**/*',
23
+ 'dist/**/*',
24
+ 'coverage/**/*',
25
+ 'src/api/graphql-operations-types.ts',
26
+ ],
22
27
  },
23
28
  // Config files and build tools (first to avoid inheritance)
24
29
  {
@@ -89,11 +94,17 @@ const config = [
89
94
  'react/no-unescaped-entities': 'off',
90
95
  '@typescript-eslint/no-unused-vars': [
91
96
  'error',
92
- { argsIgnorePattern: '^_' },
97
+ {
98
+ argsIgnorePattern: '^_',
99
+ varsIgnorePattern: '^_',
100
+ caughtErrorsIgnorePattern: '^_',
101
+ ignoreRestSiblings: true,
102
+ },
93
103
  ],
94
104
  '@typescript-eslint/explicit-function-return-type': 'off',
95
105
  '@typescript-eslint/explicit-module-boundary-types': 'off',
96
106
  '@typescript-eslint/no-explicit-any': 'off',
107
+ 'react-hooks/set-state-in-effect': 'warn',
97
108
  },
98
109
  settings: {
99
110
  react: {
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.116.7",
19
- "@salesforce/webapp-experimental": "^1.116.7",
18
+ "@salesforce/sdk-data": "^1.116.8",
19
+ "@salesforce/webapp-experimental": "^1.116.8",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "class-variance-authority": "^0.7.1",
22
22
  "clsx": "^2.1.1",
@@ -47,7 +47,7 @@
47
47
  "@graphql-eslint/eslint-plugin": "^4.1.0",
48
48
  "@graphql-tools/utils": "^11.0.0",
49
49
  "@playwright/test": "^1.49.0",
50
- "@salesforce/vite-plugin-webapp-experimental": "^1.116.7",
50
+ "@salesforce/vite-plugin-webapp-experimental": "^1.116.8",
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.1.0",
53
53
  "@testing-library/user-event": "^14.5.2",
@@ -63,9 +63,10 @@ interface PropertyMapProps {
63
63
 
64
64
  function MapCenterUpdater({ center, zoom = 13 }: { center: [number, number]; zoom?: number }) {
65
65
  const map = useMap() as { setView: (center: [number, number], zoom: number) => void };
66
+ const [lat, lng] = center;
66
67
  useEffect(() => {
67
68
  map.setView(center, zoom);
68
- }, [map, center[0], center[1], zoom]);
69
+ }, [map, center, lat, lng, zoom]);
69
70
  return null;
70
71
  }
71
72
 
@@ -86,7 +86,7 @@ export function useAuth(): AuthContextType {
86
86
  * @returns {User} The authenticated user object
87
87
  * @throws {Error} If not used within AuthProvider or user is not authenticated
88
88
  */
89
- export function getUser(): User {
89
+ export function useUser(): User {
90
90
  const context = useAuth();
91
91
  if (!context.user) {
92
92
  throw new Error("Authenticated context not established");
@@ -107,7 +107,7 @@ function formatAccessibilityAnnouncement(seconds: number): string {
107
107
  // @ts-expect-error - DurationFormat is not yet in TypeScript lib
108
108
  const formatter = new Intl.DurationFormat(navigator.language, { style: "long" });
109
109
  return formatter.format({ minutes, seconds: secs });
110
- } catch (e) {
110
+ } catch {
111
111
  // Fallback to manual formatting
112
112
  }
113
113
  }
@@ -8,7 +8,7 @@ import { useAppForm } from "../hooks/form";
8
8
  import { ROUTES } from "../authenticationConfig";
9
9
  import { emailSchema } from "../authHelpers";
10
10
  import { getErrorMessage } from "../utils/helpers";
11
- import { getUser } from "../context/AuthContext";
11
+ import { useUser } from "../context/AuthContext";
12
12
  import { fetchUserProfile, updateUserProfile } from "../api/userProfileApi";
13
13
 
14
14
  const optionalString = z
@@ -33,7 +33,7 @@ const profileSchema = z.object({
33
33
  type ProfileFormValues = z.infer<typeof profileSchema>;
34
34
 
35
35
  export default function Profile() {
36
- const user = getUser();
36
+ const user = useUser();
37
37
  const [profile, setProfile] = useState<ProfileFormValues | null>(null);
38
38
  const [loadError, setLoadError] = useState<string | null>(null);
39
39
  const [success, setSuccess] = useState(false);
@@ -104,7 +104,7 @@ export default function Profile() {
104
104
  const formData = profileSchema.parse(profile);
105
105
  form.reset(formData);
106
106
  }
107
- }, [profile]);
107
+ }, [profile, form]);
108
108
 
109
109
  if (!profile && !loadError) {
110
110
  return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
@@ -46,7 +46,7 @@ export default function Register() {
46
46
  // "/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
47
47
  // You must ensure this Apex class exists in your org and handles registration
48
48
  // (e.g., duplicate checks and user creation such as Site.createExternalUser).
49
- const { confirmPassword, ...request } = formFieldValues;
49
+ const { confirmPassword: _confirmPassword, ...request } = formFieldValues;
50
50
  const sdk = await createDataSDK();
51
51
  const response = await sdk.fetch!("/services/apexrest/auth/register", {
52
52
  method: "POST",
@@ -496,8 +496,18 @@ export default function SessionTimeoutValidator({
496
496
  // Get current location from React Router
497
497
  const location = useLocation();
498
498
 
499
- // State for session expired alert
500
- const [showExpiredAlert, setShowExpiredAlert] = useState(false);
499
+ // Session expired alert checked once at mount via lazy initializer.
500
+ // The session timeout handler triggers a hard navigation (window.location.replace),
501
+ // so the component always mounts fresh on the login page after expiry.
502
+ const [showExpiredAlert, setShowExpiredAlert] = useState(() => {
503
+ const isLoginPage = location.pathname === ROUTES.LOGIN.PATH;
504
+ const shouldShow =
505
+ isLoginPage && sessionStorage.getItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE) === "true";
506
+ if (shouldShow) {
507
+ sessionStorage.removeItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE);
508
+ }
509
+ return shouldShow;
510
+ });
501
511
 
502
512
  // Session timeout monitoring hook
503
513
  const sessionTimeout = useSessionTimeout({
@@ -505,22 +515,6 @@ export default function SessionTimeoutValidator({
505
515
  isGuest,
506
516
  });
507
517
 
508
- /**
509
- * Check if we should show expired session message
510
- * Called on mount and whenever pathname changes
511
- */
512
- useEffect(() => {
513
- // Check if we're on the login page and should show expired message
514
- const isLoginPage = location.pathname === ROUTES.LOGIN.PATH;
515
- const shouldShowMessage = sessionStorage.getItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE) === "true";
516
-
517
- if (isLoginPage && shouldShowMessage) {
518
- setShowExpiredAlert(true);
519
- // Clear the flag immediately after reading
520
- sessionStorage.removeItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE);
521
- }
522
- }, [location.pathname]);
523
-
524
518
  /**
525
519
  * Handle session extension
526
520
  * Called when user clicks "Continue Working" in warning modal
@@ -60,7 +60,7 @@ export function useFilterPanel() {
60
60
  return { hasActiveFilters: filters.length > 0, resetAll: onReset };
61
61
  }
62
62
 
63
- interface FilterResetButtonProps extends Omit<React.ComponentProps<typeof Button>, "onClick"> {}
63
+ type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
64
64
 
65
65
  export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
66
66
  const { hasActiveFilters, resetAll } = useFilterPanel();
@@ -61,7 +61,10 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
61
61
  paginationConfig?: PaginationConfig,
62
62
  ) {
63
63
  const defaultPageSize = paginationConfig?.defaultPageSize ?? 10;
64
- const validPageSizes = paginationConfig?.validPageSizes ?? [defaultPageSize];
64
+ const validPageSizes = useMemo(
65
+ () => paginationConfig?.validPageSizes ?? [defaultPageSize],
66
+ [paginationConfig?.validPageSizes, defaultPageSize],
67
+ );
65
68
  const [searchParams, setSearchParams] = useSearchParams();
66
69
 
67
70
  // Seed local state from URL on initial load
@@ -76,8 +79,10 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
76
79
  const [sort, setLocalSort] = useState<SortState | null>(initial.sort);
77
80
 
78
81
  // Pagination — cursor-based with a stack to support "previous page" navigation.
79
- const getValidPageSize = (size: number) =>
80
- validPageSizes.includes(size) ? size : defaultPageSize;
82
+ const getValidPageSize = useCallback(
83
+ (size: number) => (validPageSizes.includes(size) ? size : defaultPageSize),
84
+ [validPageSizes, defaultPageSize],
85
+ );
81
86
 
82
87
  const [pageSize, setPageSizeState] = useState<number>(
83
88
  getValidPageSize(initial.pageSize ?? defaultPageSize),
@@ -166,7 +171,7 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
166
171
  resetPagination();
167
172
  syncToUrl([], null, defaultPageSize, 0);
168
173
  setPageSizeState(defaultPageSize);
169
- }, [syncToUrl, resetPagination]);
174
+ }, [syncToUrl, resetPagination, defaultPageSize]);
170
175
 
171
176
  // -- Pagination callbacks ---------------------------------------------------
172
177
  // Uses a cursor stack to track visited pages. "Next" pushes the current
@@ -204,7 +209,7 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
204
209
  resetPagination();
205
210
  debouncedSyncRef.current(f, s, validated);
206
211
  },
207
- [resetPagination],
212
+ [resetPagination, getValidPageSize],
208
213
  );
209
214
 
210
215
  // -- Derived query objects ---------------------------------------------------
@@ -7,16 +7,17 @@ export function useGeocode(address: string | null | undefined): {
7
7
  } {
8
8
  const [coords, setCoords] = useState<GeocodeResult | null>(null);
9
9
  const [loading, setLoading] = useState(false);
10
+ const trimmedAddress = address?.trim() ?? "";
10
11
 
11
12
  useEffect(() => {
12
- if (!address?.trim()) {
13
+ if (!trimmedAddress) {
13
14
  setCoords(null);
14
15
  setLoading(false);
15
16
  return;
16
17
  }
17
18
  let cancelled = false;
18
19
  setLoading(true);
19
- const normalized = address.replace(/\n/g, ", ").trim();
20
+ const normalized = address!.replace(/\n/g, ", ").trim();
20
21
 
21
22
  (async () => {
22
23
  try {
@@ -42,7 +43,7 @@ export function useGeocode(address: string | null | undefined): {
42
43
  return () => {
43
44
  cancelled = true;
44
45
  };
45
- }, [address?.trim() ?? ""]);
46
+ }, [trimmedAddress]);
46
47
 
47
48
  return { coords, loading };
48
49
  }
@@ -23,17 +23,18 @@ export function usePropertyListingAmenities(
23
23
  const loading = idsKey !== "" && idsKey !== fetchedKey;
24
24
 
25
25
  useEffect(() => {
26
- if (uniqueIds.length === 0) {
26
+ const ids = idsKey === "" ? [] : idsKey.split(",");
27
+ if (ids.length === 0) {
27
28
  setMap({});
28
29
  setFetchedKey("");
29
30
  return;
30
31
  }
31
32
  let cancelled = false;
32
- Promise.all(uniqueIds.map((id) => fetchFeaturesByPropertyId(id)))
33
+ Promise.all(ids.map((id) => fetchFeaturesByPropertyId(id)))
33
34
  .then((featuresPerProperty) => {
34
35
  if (cancelled) return;
35
36
  const next: Record<string, string> = {};
36
- uniqueIds.forEach((id, i) => {
37
+ ids.forEach((id, i) => {
37
38
  const features = featuresPerProperty[i] ?? [];
38
39
  const descriptions = features
39
40
  .map((f) => f.description)
@@ -2,7 +2,7 @@
2
2
  * Fetches property addresses for the current page of results only, geocodes them in parallel,
3
3
  * and returns map markers (one pin per property in the current window).
4
4
  */
5
- import { useState, useEffect } from "react";
5
+ import { useState, useEffect, useRef } from "react";
6
6
  import { fetchPropertyAddresses } from "@/api/properties/propertyDetailGraphQL";
7
7
  import { geocodeAddress, getStateZipFromAddress } from "@/utils/geocode";
8
8
  import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
@@ -94,19 +94,29 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
94
94
  propertyIdToLabel.set(id, getListingName(r.record));
95
95
  }
96
96
  }
97
+ const idsKey = [...new Set(propertyIds)].join(",");
97
98
 
99
+ const resultsRef = useRef(results);
100
+ const labelMapRef = useRef(propertyIdToLabel);
98
101
  useEffect(() => {
99
- if (propertyIds.length === 0) {
102
+ resultsRef.current = results;
103
+ labelMapRef.current = propertyIdToLabel;
104
+ });
105
+
106
+ useEffect(() => {
107
+ const uniqIds = idsKey === "" ? [] : idsKey.split(",");
108
+ if (uniqIds.length === 0) {
100
109
  setMarkers([]);
101
110
  setLoading(false);
102
111
  return;
103
112
  }
104
113
  let cancelled = false;
105
114
  setLoading(true);
106
- const uniqIds = [...new Set(propertyIds)];
115
+ const currentResults = resultsRef.current;
116
+ const currentLabels = labelMapRef.current;
107
117
  const directMarkers: MapMarker[] = [];
108
118
  const missingIds: string[] = [];
109
- for (const r of results) {
119
+ for (const r of currentResults) {
110
120
  if (!r?.record) continue;
111
121
  const id = getPropertyIdFromRecord(r.record);
112
122
  if (!id || !uniqIds.includes(id)) continue;
@@ -115,7 +125,7 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
115
125
  directMarkers.push({
116
126
  lat: coords.lat,
117
127
  lng: coords.lng,
118
- label: propertyIdToLabel.get(id) ?? "Property",
128
+ label: currentLabels.get(id) ?? "Property",
119
129
  propertyId: id,
120
130
  });
121
131
  }
@@ -140,7 +150,6 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
140
150
  setLoading(false);
141
151
  return;
142
152
  }
143
- // Geocode all addresses in parallel; fallback to City, State Zip if full address fails
144
153
  Promise.all(
145
154
  toGeocode.map(async ([id, address]) => {
146
155
  const normalized = address.replace(/\n/g, ", ").trim();
@@ -161,7 +170,7 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
161
170
  .map(({ id, coords }) => ({
162
171
  lat: coords.lat,
163
172
  lng: coords.lng,
164
- label: propertyIdToLabel.get(id) ?? "Property",
173
+ label: currentLabels.get(id) ?? "Property",
165
174
  propertyId: id,
166
175
  }));
167
176
  setMarkers(spreadDuplicateMarkers([...directMarkers, ...geocoded]));
@@ -182,7 +191,7 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
182
191
  return () => {
183
192
  cancelled = true;
184
193
  };
185
- }, [propertyIds.join(",")]);
194
+ }, [idsKey]);
186
195
 
187
196
  return { markers, loading };
188
197
  }
@@ -28,13 +28,14 @@ export function usePropertyPrimaryImages(
28
28
  const loading = idsKey !== "" && idsKey !== fetchedKey;
29
29
 
30
30
  useEffect(() => {
31
- if (propertyIds.length === 0) {
31
+ const ids = idsKey === "" ? [] : idsKey.split(",");
32
+ if (ids.length === 0) {
32
33
  setMap({});
33
34
  setFetchedKey("");
34
35
  return;
35
36
  }
36
37
  let cancelled = false;
37
- fetchPrimaryImagesByPropertyIds(propertyIds)
38
+ fetchPrimaryImagesByPropertyIds(ids)
38
39
  .then((next) => {
39
40
  if (!cancelled) setMap(next);
40
41
  })
@@ -268,17 +268,14 @@ interface GeoPosition {
268
268
  }
269
269
 
270
270
  function useGeolocation(): GeoPosition {
271
- const [position, setPosition] = useState<GeoPosition>({
271
+ const [position, setPosition] = useState<GeoPosition>(() => ({
272
272
  latitude: FALLBACK.LAT,
273
273
  longitude: FALLBACK.LNG,
274
- resolved: false,
275
- });
274
+ resolved: typeof navigator === "undefined" || !navigator.geolocation,
275
+ }));
276
276
 
277
277
  useEffect(() => {
278
- if (!navigator.geolocation) {
279
- setPosition((prev) => ({ ...prev, resolved: true }));
280
- return;
281
- }
278
+ if (!navigator.geolocation) return;
282
279
  navigator.geolocation.getCurrentPosition(
283
280
  (pos) =>
284
281
  setPosition({
@@ -141,7 +141,7 @@ export default function Application() {
141
141
  setSubmitting(false);
142
142
  }
143
143
  },
144
- [propertyId, contactId, moveInDate, employment, references],
144
+ [propertyId, contactId, moveInDate, employment, references, user?.id],
145
145
  );
146
146
 
147
147
  if (loading) {
@@ -75,8 +75,6 @@ export default function Maintenance() {
75
75
  const [submitError, setSubmitError] = useState<string | null>(null);
76
76
  const [submitSuccess, setSubmitSuccess] = useState(false);
77
77
 
78
- if (authLoading) return <MaintenanceSkeleton />;
79
-
80
78
  const handleSubmit = useCallback(
81
79
  async (e: React.FormEvent) => {
82
80
  e.preventDefault();
@@ -113,6 +111,8 @@ export default function Maintenance() {
113
111
  [title, description, type, priority, dateRequested, refetch],
114
112
  );
115
113
 
114
+ if (authLoading) return <MaintenanceSkeleton />;
115
+
116
116
  return (
117
117
  <div className="mx-auto max-w-[900px]">
118
118
  <Card className="mb-6 rounded-2xl shadow-md">
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.116.7",
3
+ "version": "1.116.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
9
- "version": "1.116.7",
9
+ "version": "1.116.8",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
11
  "devDependencies": {
12
12
  "@lwc/eslint-plugin-lwc": "^3.3.0",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.116.7",
3
+ "version": "1.116.8",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-sample-b2x-experimental",
3
- "version": "1.116.7",
3
+ "version": "1.116.8",
4
4
  "description": "Salesforce sample property rental React app",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -32,6 +32,9 @@
32
32
  "watch": {
33
33
  "executor": "@salesforce/webapp-template-cli-experimental:watch-patches"
34
34
  },
35
+ "build:dist-app": {
36
+ "executor": "@salesforce/webapp-template-cli-experimental:build-dist-app"
37
+ },
35
38
  "dev": {
36
39
  "executor": "@salesforce/webapp-template-cli-experimental:dev-server"
37
40
  }