@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 +16 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/codegen.yml +82 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/api/graphql-operations-types.ts +24 -37
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/authentication-route.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/private-route.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/layout/card-skeleton.tsx +38 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/skeleton.tsx +8 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Profile.tsx +50 -19
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/styles/global.css +88 -50
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/utils/helpers.ts +17 -9
- package/dist/package.json +1 -1
- package/package.json +3 -3
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/layout/loading-page.tsx +0 -46
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
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/codegen.yml
CHANGED
|
@@ -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:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
47
|
-
|
|
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:
|
|
51
|
-
LongTextArea: { input:
|
|
52
|
-
Longitude: { input:
|
|
53
|
-
MultiPicklist: { input:
|
|
54
|
-
Percent: { input:
|
|
55
|
-
PhoneNumber: { input:
|
|
56
|
-
Picklist: { input:
|
|
57
|
-
RichTextArea: { input:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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?:
|
|
122
|
-
Industry?: { value?:
|
|
123
|
-
Website?: { value?:
|
|
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 {
|
|
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 <
|
|
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 {
|
|
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 <
|
|
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.
|
|
4
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 <
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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]
|
|
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 -
|
|
144
|
-
* @
|
|
145
|
-
* @
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-feature-react-authentication-experimental",
|
|
3
|
-
"version": "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.
|
|
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": "
|
|
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
|
-
}
|