@salesforce/webapp-template-feature-react-authentication-experimental 1.38.1 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md 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.40.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.39.0...v1.40.0) (2026-02-20)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ # [1.39.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.38.1...v1.39.0) (2026-02-19)
15
+
16
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
17
+
18
+
19
+
20
+
21
+
6
22
  ## [1.38.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.38.0...v1.38.1) (2026-02-19)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -9,4 +9,86 @@ generates:
9
9
  onlyOperationTypes: true
10
10
  skipTypename: true
11
11
  preResolveTypes: true
12
+ scalars:
13
+ # String-serialized scalars
14
+ JSON:
15
+ input: 'string'
16
+ output: 'string'
17
+ Date:
18
+ input: 'string'
19
+ output: 'string'
20
+ DateTime:
21
+ input: 'string'
22
+ output: 'string'
23
+ Time:
24
+ input: 'string'
25
+ output: 'string'
26
+ Email:
27
+ input: 'string'
28
+ output: 'string'
29
+ Url:
30
+ input: 'string'
31
+ output: 'string'
32
+ PhoneNumber:
33
+ input: 'string'
34
+ output: 'string'
35
+ Picklist:
36
+ input: 'string'
37
+ output: 'string'
38
+ MultiPicklist:
39
+ input: 'string'
40
+ output: 'string'
41
+ TextArea:
42
+ input: 'string'
43
+ output: 'string'
44
+ LongTextArea:
45
+ input: 'string'
46
+ output: 'string'
47
+ RichTextArea:
48
+ input: 'string'
49
+ output: 'string'
50
+ EncryptedString:
51
+ input: 'string'
52
+ output: 'string'
53
+ Base64:
54
+ input: 'string'
55
+ output: 'string'
56
+ IdOrRef:
57
+ input: 'string'
58
+ output: 'string'
59
+ # BigDecimal-serialized scalars (accepts number or string, returns number)
60
+ Currency:
61
+ input: 'number | string'
62
+ output: 'number'
63
+ BigDecimal:
64
+ input: 'number | string'
65
+ output: 'number'
66
+ Double:
67
+ input: 'number | string'
68
+ output: 'number'
69
+ Percent:
70
+ input: 'number | string'
71
+ output: 'number'
72
+ Longitude:
73
+ input: 'number | string'
74
+ output: 'number'
75
+ Latitude:
76
+ input: 'number | string'
77
+ output: 'number'
78
+ # Integer-like scalars
79
+ Long:
80
+ input: 'number'
81
+ output: 'number'
82
+ BigInteger:
83
+ input: 'number'
84
+ output: 'number'
85
+ Short:
86
+ input: 'number'
87
+ output: 'number'
88
+ Byte:
89
+ input: 'number'
90
+ output: 'number'
91
+ Char:
92
+ input: 'number'
93
+ output: 'number'
12
94
  overwrite: true
@@ -12,9 +12,7 @@ export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
12
12
  export type MakeEmpty<
13
13
  T extends { [key: string]: unknown },
14
14
  K extends keyof T,
15
- > = {
16
- [_ in K]?: never;
17
- };
15
+ > = { [_ in K]?: never };
18
16
  export type Incremental<T> =
19
17
  | T
20
18
  | {
@@ -27,39 +25,28 @@ export type Scalars = {
27
25
  Boolean: { input: boolean; output: boolean };
28
26
  Int: { input: number; output: number };
29
27
  Float: { input: number; output: number };
30
- Base64: { input: any; output: any };
31
- /** An arbitrary precision signed decimal */
32
- BigDecimal: { input: any; output: any };
33
- /** An arbitrary precision signed integer */
34
- BigInteger: { input: any; output: any };
35
- /** An 8-bit signed integer */
36
- Byte: { input: any; output: any };
37
- /** A UTF-16 code unit; a character on Unicode's BMP */
38
- Char: { input: any; output: any };
39
- Currency: { input: any; output: any };
40
- Date: { input: any; output: any };
41
- DateTime: { input: any; output: any };
42
- Double: { input: any; output: any };
43
- Email: { input: any; output: any };
44
- EncryptedString: { input: any; output: any };
28
+ Base64: { input: string; output: string };
29
+ Currency: { input: number | string; output: number };
30
+ Date: { input: string; output: string };
31
+ DateTime: { input: string; output: string };
32
+ Double: { input: number | string; output: number };
33
+ Email: { input: string; output: string };
34
+ EncryptedString: { input: string; output: string };
45
35
  /** Can be set to an ID or a Reference to the result of another mutation operation. */
46
- IdOrRef: { input: any; output: any };
47
- JSON: { input: any; output: any };
48
- Latitude: { input: any; output: any };
36
+ IdOrRef: { input: string; output: string };
37
+ Latitude: { input: number | string; output: number };
49
38
  /** A 64-bit signed integer */
50
- Long: { input: any; output: any };
51
- LongTextArea: { input: any; output: any };
52
- Longitude: { input: any; output: any };
53
- MultiPicklist: { input: any; output: any };
54
- Percent: { input: any; output: any };
55
- PhoneNumber: { input: any; output: any };
56
- Picklist: { input: any; output: any };
57
- RichTextArea: { input: any; output: any };
58
- /** A 16-bit signed integer */
59
- Short: { input: any; output: any };
60
- TextArea: { input: any; output: any };
61
- Time: { input: any; output: any };
62
- Url: { input: any; output: any };
39
+ Long: { input: number; output: number };
40
+ LongTextArea: { input: string; output: string };
41
+ Longitude: { input: number | string; output: number };
42
+ MultiPicklist: { input: string; output: string };
43
+ Percent: { input: number | string; output: number };
44
+ PhoneNumber: { input: string; output: string };
45
+ Picklist: { input: string; output: string };
46
+ RichTextArea: { input: string; output: string };
47
+ TextArea: { input: string; output: string };
48
+ Time: { input: string; output: string };
49
+ Url: { input: string; output: string };
63
50
  };
64
51
 
65
52
  export enum DataType {
@@ -118,9 +105,9 @@ export type GetHighRevenueAccountsQuery = {
118
105
  node?: {
119
106
  Id: string;
120
107
  Name?: { value?: string | null } | null;
121
- AnnualRevenue?: { value?: any | null } | null;
122
- Industry?: { value?: any | null } | null;
123
- Website?: { value?: any | null } | null;
108
+ AnnualRevenue?: { value?: number | null } | null;
109
+ Industry?: { value?: string | null } | null;
110
+ Website?: { value?: string | null } | null;
124
111
  } | null;
125
112
  } | null> | null;
126
113
  } | null;
@@ -1,7 +1,7 @@
1
1
  import { Navigate, Outlet, useSearchParams } from "react-router";
2
2
  import { useAuth } from "../../context/AuthContext";
3
3
  import { getStartUrl } from "../../utils/helpers";
4
- import { LoadingPage } from "../layout/loading-page";
4
+ import { CardSkeleton } from "../layout/card-skeleton";
5
5
 
6
6
  /**
7
7
  * [Dev Note] "Public Only" Route Guard:
@@ -14,7 +14,7 @@ export default function AuthenticationRoute() {
14
14
  const { isAuthenticated, loading } = useAuth();
15
15
  const [searchParams] = useSearchParams();
16
16
 
17
- if (loading) return <LoadingPage contentMaxWidth="md" />;
17
+ if (loading) return <CardSkeleton contentMaxWidth="md" />;
18
18
  if (isAuthenticated) return <Navigate to={getStartUrl(searchParams)} replace />;
19
19
 
20
20
  return <Outlet />;
@@ -1,7 +1,7 @@
1
1
  import { Navigate, Outlet, useLocation } from "react-router";
2
2
  import { useAuth } from "../../context/AuthContext";
3
3
  import { AUTH_REDIRECT_PARAM, ROUTES } from "../../utils/authenticationConfig";
4
- import { LoadingPage } from "../layout/loading-page";
4
+ import { CardSkeleton } from "../layout/card-skeleton";
5
5
 
6
6
  /**
7
7
  * [Dev Note] Route Guard:
@@ -13,7 +13,7 @@ export default function PrivateRoute() {
13
13
  const { isAuthenticated, loading } = useAuth();
14
14
  const location = useLocation();
15
15
 
16
- if (loading) return <LoadingPage contentMaxWidth="md" />;
16
+ if (loading) return <CardSkeleton contentMaxWidth="md" />;
17
17
 
18
18
  if (!isAuthenticated) {
19
19
  const searchParams = new URLSearchParams();
@@ -0,0 +1,38 @@
1
+ import { CenteredPageLayout } from "./centered-page-layout";
2
+ import { Card, CardContent, CardHeader } from "../ui/card";
3
+ import { Skeleton } from "../ui/skeleton";
4
+
5
+ interface CardSkeletonProps {
6
+ /**
7
+ * Maximum width of the content container.
8
+ * @default "sm"
9
+ */
10
+ contentMaxWidth?: "sm" | "md" | "lg";
11
+ /**
12
+ * Accessible label for screen readers.
13
+ * @default "Loading…"
14
+ */
15
+ loadingText?: string;
16
+ }
17
+
18
+ /**
19
+ * Full-page loading indicator with skeleton card placeholder.
20
+ */
21
+ export function CardSkeleton({ contentMaxWidth, loadingText = "Loading…" }: CardSkeletonProps) {
22
+ return (
23
+ <CenteredPageLayout contentMaxWidth={contentMaxWidth}>
24
+ <div role="status" aria-live="polite">
25
+ <Card className="w-full">
26
+ <CardHeader>
27
+ <Skeleton className="h-4 w-2/3" />
28
+ <Skeleton className="h-4 w-1/2" />
29
+ </CardHeader>
30
+ <CardContent>
31
+ <Skeleton className="aspect-video w-full" />
32
+ </CardContent>
33
+ </Card>
34
+ <span className="sr-only">{loadingText}</span>
35
+ </div>
36
+ </CenteredPageLayout>
37
+ );
38
+ }
@@ -1,7 +1,13 @@
1
1
  import { cn } from "../../lib/utils";
2
2
 
3
- function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
4
- return <div className={cn("animate-pulse rounded-md bg-gray-200", className)} {...props} />;
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("bg-accent animate-pulse rounded-md", className)}
8
+ {...props}
9
+ />
10
+ );
5
11
  }
6
12
 
7
13
  export { Skeleton };
@@ -1,18 +1,50 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import { z } from "zod";
3
3
 
4
- // [Dev Note] These are standard Salesforce SDK methods that interact with the UI API.
5
- // They respect field-level security and validation rules defined in Salesforce.
6
- import { getRecord, updateRecord } from "@salesforce/webapp-experimental/api";
4
+ import { executeGraphQL } from "@salesforce/webapp-experimental/api";
7
5
 
8
6
  import { CenteredPageLayout } from "../components/layout/centered-page-layout";
9
- import { LoadingPage } from "../components/layout/loading-page";
7
+ import { CardSkeleton } from "../components/layout/card-skeleton";
10
8
  import { AuthForm } from "../components/forms/auth-form";
11
9
  import { useAppForm } from "../hooks/form";
12
10
  import { ROUTES } from "../utils/authenticationConfig";
13
- import { emailSchema, flattenUiApiRecord, getErrorMessage } from "../utils/helpers";
11
+ import { emailSchema, flattenGraphQLRecord, getErrorMessage } from "../utils/helpers";
14
12
  import { getUser } from "../context/AuthContext";
15
13
 
14
+ const GRAPHQL_USER_PROFILE_FIELDS = `
15
+ Id
16
+ FirstName { value }
17
+ LastName { value }
18
+ Email { value }
19
+ Phone { value }
20
+ Street { value }
21
+ City { value }
22
+ State { value }
23
+ PostalCode { value }
24
+ Country { value }`;
25
+
26
+ const QUERY_PROFILE_GRAPHQL = `
27
+ query GetUserProfile($userId: ID) {
28
+ uiapi {
29
+ query {
30
+ User(where: { Id: { eq: $userId } }) {
31
+ edges {
32
+ node {${GRAPHQL_USER_PROFILE_FIELDS}}
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }`;
38
+
39
+ const MUTATE_PROFILE_GRAPHQL = `
40
+ mutation UpdateUserProfile($input: UserUpdateInput!) {
41
+ uiapi {
42
+ UserUpdate(input: $input) {
43
+ Record {${GRAPHQL_USER_PROFILE_FIELDS}}
44
+ }
45
+ }
46
+ }`;
47
+
16
48
  const optionalString = z
17
49
  .string()
18
50
  .trim()
@@ -48,12 +80,10 @@ export default function Profile() {
48
80
  setSubmitError(null);
49
81
  setSuccess(false);
50
82
  try {
51
- // [Dev Note] updateRecord automatically handles the PATCH request to the UI API.
52
- // It expects the Record ID (user.id) and an object of field values.
53
- const record = await updateRecord(user.id, value);
54
-
55
- // [Dev Note] We flatten the complex UI API response structure for easier local use.
56
- setProfile(flattenUiApiRecord(record));
83
+ const result: any = await executeGraphQL(MUTATE_PROFILE_GRAPHQL, {
84
+ input: { Id: user.id, User: { ...value } },
85
+ });
86
+ setProfile(flattenGraphQLRecord(result?.uiapi?.UserUpdate?.Record));
57
87
 
58
88
  setSuccess(true);
59
89
  // Scroll to top of page after successful update so user sees it
@@ -68,10 +98,11 @@ export default function Profile() {
68
98
  useEffect(() => {
69
99
  let mounted = true;
70
100
 
71
- // [Dev Note] Fetch the user record fields. "layoutTypes: 'Full'" asks for the default layout fields.
72
- // Ensure the authenticated user has Read access to these fields in Salesforce.
73
- getRecord(user.id, { layoutTypes: "Full" })
74
- .then((record: any) => mounted && setProfile(flattenUiApiRecord(record)))
101
+ executeGraphQL(QUERY_PROFILE_GRAPHQL, { userId: user.id })
102
+ .then(
103
+ (result: any) =>
104
+ mounted && setProfile(flattenGraphQLRecord(result?.uiapi?.query?.User?.edges?.[0]?.node)),
105
+ )
75
106
  .catch((err: any) => {
76
107
  if (mounted) {
77
108
  setLoadError(getErrorMessage(err, "Failed to load profile"));
@@ -92,7 +123,7 @@ export default function Profile() {
92
123
  }, [profile]);
93
124
 
94
125
  if (!profile && !loadError) {
95
- return <LoadingPage contentMaxWidth="md" loadingText="Loading profile…" />;
126
+ return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
96
127
  }
97
128
 
98
129
  return (
@@ -106,6 +137,9 @@ export default function Profile() {
106
137
  submit={{ text: "Save Changes", loadingText: "Saving…" }}
107
138
  footer={{ link: ROUTES.CHANGE_PASSWORD.PATH, linkText: "Change password" }}
108
139
  >
140
+ <form.AppField name="Email">
141
+ {(field) => <field.EmailField label="Email" disabled />}
142
+ </form.AppField>
109
143
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
110
144
  <form.AppField name="FirstName">
111
145
  {(field) => <field.TextField label="First name" autoComplete="given-name" />}
@@ -114,9 +148,6 @@ export default function Profile() {
114
148
  {(field) => <field.TextField label="Last name" autoComplete="family-name" />}
115
149
  </form.AppField>
116
150
  </div>
117
- <form.AppField name="Email">
118
- {(field) => <field.EmailField label="Email" />}
119
- </form.AppField>
120
151
  <form.AppField name="Phone">
121
152
  {(field) => <field.TextField label="Phone" type="tel" autoComplete="tel" />}
122
153
  </form.AppField>
@@ -1,56 +1,80 @@
1
1
  @import "tailwindcss";
2
- @layer base {
3
- :root {
4
- /* browser system elements (scrollbars, etc) */
5
- color-scheme: light;
6
2
 
7
- --background: oklch(1 0 0);
8
- --foreground: oklch(0.145 0 0);
9
- --card: oklch(1 0 0);
10
- --card-foreground: oklch(0.145 0 0);
11
- --popover: oklch(1 0 0);
12
- --popover-foreground: oklch(0.145 0 0);
13
- --primary: oklch(0.205 0 0);
14
- --primary-foreground: oklch(0.985 0 0);
15
- --secondary: oklch(0.97 0 0);
16
- --secondary-foreground: oklch(0.205 0 0);
17
- --muted: oklch(0.97 0 0);
18
- --muted-foreground: oklch(0.556 0 0);
19
- --accent: oklch(0.97 0 0);
20
- --accent-foreground: oklch(0.205 0 0);
21
- --destructive: oklch(0.577 0.245 27.325);
22
- /* 1. Maintenance: Added missing token for accessible text on red backgrounds */
23
- --destructive-foreground: oklch(0.985 0 0);
24
- --border: oklch(0.922 0 0);
25
- --input: oklch(0.922 0 0);
26
- --ring: oklch(0.205 0 0);
27
- --radius: 0.625rem;
28
- --spacing: 0.25rem;
29
- }
30
- .dark {
31
- /* browser system elements */
32
- color-scheme: dark;
3
+ :root {
4
+ color-scheme: light;
5
+ --radius: 0.625rem;
6
+ --spacing: 0.25rem;
7
+ --background: oklch(1 0 0);
8
+ --foreground: oklch(0.145 0 0);
9
+ --card: oklch(1 0 0);
10
+ --card-foreground: oklch(0.145 0 0);
11
+ --popover: oklch(1 0 0);
12
+ --popover-foreground: oklch(0.145 0 0);
13
+ --primary: oklch(0.205 0 0);
14
+ --primary-foreground: oklch(0.985 0 0);
15
+ --secondary: oklch(0.97 0 0);
16
+ --secondary-foreground: oklch(0.205 0 0);
17
+ --muted: oklch(0.97 0 0);
18
+ --muted-foreground: oklch(0.556 0 0);
19
+ --accent: oklch(0.97 0 0);
20
+ --accent-foreground: oklch(0.205 0 0);
21
+ --destructive: oklch(0.577 0.245 27.325);
22
+ --destructive-foreground: oklch(0.985 0 0);
23
+ --border: oklch(0.922 0 0);
24
+ --input: oklch(0.922 0 0);
25
+ --ring: oklch(0.708 0 0);
26
+ --chart-1: oklch(0.646 0.222 41.116);
27
+ --chart-2: oklch(0.6 0.118 184.704);
28
+ --chart-3: oklch(0.398 0.07 227.392);
29
+ --chart-4: oklch(0.828 0.189 84.429);
30
+ --chart-5: oklch(0.769 0.188 70.08);
31
+ --sidebar: oklch(0.985 0 0);
32
+ --sidebar-foreground: oklch(0.145 0 0);
33
+ --sidebar-primary: oklch(0.205 0 0);
34
+ --sidebar-primary-foreground: oklch(0.985 0 0);
35
+ --sidebar-accent: oklch(0.97 0 0);
36
+ --sidebar-accent-foreground: oklch(0.205 0 0);
37
+ --sidebar-border: oklch(0.922 0 0);
38
+ --sidebar-ring: oklch(0.708 0 0);
39
+ }
33
40
 
34
- --background: oklch(0.145 0 0);
35
- --foreground: oklch(0.985 0 0);
36
- --card: oklch(0.145 0 0);
37
- --card-foreground: oklch(0.985 0 0);
38
- --popover: oklch(0.145 0 0);
39
- --popover-foreground: oklch(0.985 0 0);
40
- --primary: oklch(0.985 0 0);
41
- --primary-foreground: oklch(0.205 0 0);
42
- --secondary: oklch(0.269 0 0);
43
- --secondary-foreground: oklch(0.985 0 0);
44
- --muted: oklch(0.269 0 0);
45
- --muted-foreground: oklch(0.708 0 0);
46
- --accent: oklch(0.269 0 0);
47
- --accent-foreground: oklch(0.985 0 0);
48
- --destructive: oklch(0.396 0.141 25.723);
49
- /* Dark mode text for destructive actions */
50
- --destructive-foreground: oklch(0.985 0 0);
51
- --accent-foreground: oklch(0.985 0 0);
52
- }
53
- /* 2. Accessibility: Global reset for users who prefer reduced motion */
41
+ .dark {
42
+ color-scheme: dark;
43
+ --background: oklch(0.145 0 0);
44
+ --foreground: oklch(0.985 0 0);
45
+ --card: oklch(0.205 0 0);
46
+ --card-foreground: oklch(0.985 0 0);
47
+ --popover: oklch(0.269 0 0);
48
+ --popover-foreground: oklch(0.985 0 0);
49
+ --primary: oklch(0.922 0 0);
50
+ --primary-foreground: oklch(0.205 0 0);
51
+ --secondary: oklch(0.269 0 0);
52
+ --secondary-foreground: oklch(0.985 0 0);
53
+ --muted: oklch(0.269 0 0);
54
+ --muted-foreground: oklch(0.708 0 0);
55
+ --accent: oklch(0.371 0 0);
56
+ --accent-foreground: oklch(0.985 0 0);
57
+ --destructive: oklch(0.704 0.191 22.216);
58
+ --destructive-foreground: oklch(0.985 0 0);
59
+ --border: oklch(1 0 0 / 10%);
60
+ --input: oklch(1 0 0 / 15%);
61
+ --ring: oklch(0.556 0 0);
62
+ --chart-1: oklch(0.488 0.243 264.376);
63
+ --chart-2: oklch(0.696 0.17 162.48);
64
+ --chart-3: oklch(0.769 0.188 70.08);
65
+ --chart-4: oklch(0.627 0.265 303.9);
66
+ --chart-5: oklch(0.645 0.246 16.439);
67
+ --sidebar: oklch(0.205 0 0);
68
+ --sidebar-foreground: oklch(0.985 0 0);
69
+ --sidebar-primary: oklch(0.488 0.243 264.376);
70
+ --sidebar-primary-foreground: oklch(0.985 0 0);
71
+ --sidebar-accent: oklch(0.269 0 0);
72
+ --sidebar-accent-foreground: oklch(0.985 0 0);
73
+ --sidebar-border: oklch(1 0 0 / 10%);
74
+ --sidebar-ring: oklch(0.439 0 0);
75
+ }
76
+
77
+ @layer base {
54
78
  @media (prefers-reduced-motion: reduce) {
55
79
  * {
56
80
  animation-duration: 0.01ms !important;
@@ -83,9 +107,23 @@
83
107
  --color-accent: var(--accent);
84
108
  --color-accent-foreground: var(--accent-foreground);
85
109
  --color-destructive: var(--destructive);
110
+ --color-destructive-foreground: var(--destructive-foreground);
86
111
  --color-border: var(--border);
87
112
  --color-input: var(--input);
88
113
  --color-ring: var(--ring);
114
+ --color-chart-1: var(--chart-1);
115
+ --color-chart-2: var(--chart-2);
116
+ --color-chart-3: var(--chart-3);
117
+ --color-chart-4: var(--chart-4);
118
+ --color-chart-5: var(--chart-5);
119
+ --color-sidebar: var(--sidebar);
120
+ --color-sidebar-foreground: var(--sidebar-foreground);
121
+ --color-sidebar-primary: var(--sidebar-primary);
122
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
123
+ --color-sidebar-accent: var(--sidebar-accent);
124
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
125
+ --color-sidebar-border: var(--sidebar-border);
126
+ --color-sidebar-ring: var(--sidebar-ring);
89
127
  --radius-sm: calc(var(--radius) - 4px);
90
128
  --radius-md: calc(var(--radius) - 2px);
91
129
  --radius-lg: var(--radius);
@@ -136,21 +136,29 @@ export type RecordResponse = {
136
136
  };
137
137
 
138
138
  /**
139
- * [Dev Note] The UI API returns a complex nested structure.
139
+ * [Dev Note] GraphQL can return a complex nested structure.
140
140
  * This helper flattens it to a simple object for easier form binding.
141
- * Flattens { fields: { FieldName: { displayValue, value } } } to { FieldName: value }
142
141
  *
143
- * @param data - The RecordResponse with field objects.
144
- * @throws {Error} If data is not a valid RecordResponse.
145
- * @returns Flattened object with field values.
142
+ * @param data - Extracted payload from the GraphQL response.
143
+ * @param fallbackError - Fallback error message if data is null/undefined or not an object.
144
+ * @throws {Error} If data is not valid.
145
+ * @returns Flattened object with values mapped directly to the fields.
146
146
  */
147
- export function flattenUiApiRecord<T>(data: RecordResponse): T {
148
- if (!data?.fields) {
149
- throw new Error(parseApiResponseError(data));
147
+ export function flattenGraphQLRecord<T>(
148
+ data: any,
149
+ fallbackError: string = "An unknown error occurred",
150
+ ): T {
151
+ if (!data || typeof data !== "object") {
152
+ throw new Error(fallbackError);
150
153
  }
151
154
 
152
155
  return Object.fromEntries(
153
- Object.entries(data.fields).map(([key, field]) => [key, field?.value ?? null]),
156
+ Object.entries(data).map(([key, field]) => [
157
+ key,
158
+ field !== null && typeof field === "object" && "value" in field
159
+ ? (field as { value: unknown }).value
160
+ : (field ?? null),
161
+ ]),
154
162
  ) as T;
155
163
  }
156
164
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-feature-react-authentication-experimental",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "description": "Authentication feature for web applications",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -18,7 +18,7 @@
18
18
  "watch": "npx tsx ../../cli/src/index.ts watch-patches packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app packages/template/feature/feature-react-authentication/dist"
19
19
  },
20
20
  "devDependencies": {
21
- "@salesforce/webapp-experimental": "^1.38.1",
21
+ "@salesforce/webapp-experimental": "^1.40.0",
22
22
  "@tanstack/react-form": "^1.27.7",
23
23
  "@types/react": "^19.2.7",
24
24
  "@types/react-dom": "^19.2.3",
@@ -36,5 +36,5 @@
36
36
  }
37
37
  }
38
38
  },
39
- "gitHead": "2c6b1587a57f8cf01f5150f21bc0e7c0a52507fd"
39
+ "gitHead": "fb08cea2853492f373c8267f2327f8c8085504ef"
40
40
  }
@@ -1,46 +0,0 @@
1
- import { cva, type VariantProps } from "class-variance-authority";
2
- import { CenteredPageLayout } from "./centered-page-layout";
3
- import { Spinner } from "../ui/spinner";
4
-
5
- /**
6
- * Spinner size variants based on content width.
7
- */
8
- const spinnerVariants = cva("", {
9
- variants: {
10
- contentMaxWidth: {
11
- sm: "size-6",
12
- md: "size-8",
13
- lg: "size-10",
14
- },
15
- },
16
- defaultVariants: {
17
- contentMaxWidth: "sm",
18
- },
19
- });
20
-
21
- interface LoadingPageProps extends VariantProps<typeof spinnerVariants> {
22
- /**
23
- * Maximum width of the content container. Also scales the spinner size.
24
- * @default "sm"
25
- */
26
- contentMaxWidth?: "sm" | "md" | "lg";
27
- /**
28
- * Accessible label for screen readers.
29
- * @default "Loading…"
30
- */
31
- loadingText?: string;
32
- }
33
-
34
- /**
35
- * Full-page loading indicator with centered spinner.
36
- */
37
- export function LoadingPage({ contentMaxWidth, loadingText = "Loading…" }: LoadingPageProps) {
38
- return (
39
- <CenteredPageLayout contentMaxWidth={contentMaxWidth}>
40
- <div className="flex justify-center" role="status" aria-live="polite">
41
- <Spinner className={spinnerVariants({ contentMaxWidth })} aria-hidden="true" />
42
- <span className="sr-only">{loadingText}</span>
43
- </div>
44
- </CenteredPageLayout>
45
- );
46
- }