@salesforce/ui-bundle-template-app-react-template-b2x 3.0.0 → 3.1.1
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 +17 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/components/alerts/status-alert.tsx +11 -8
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/components/ui/input.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/api/userProfileApi.ts +2 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/authenticationConfig.ts +9 -9
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/context/AuthContext.tsx +21 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/forms/auth-form.tsx +15 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/layouts/privateRouteLayout.tsx +2 -11
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/ChangePassword.tsx +20 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/ForgotPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/Login.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/Profile.tsx +80 -43
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/Register.tsx +15 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/pages/ResetPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/utils/helpers.ts +15 -52
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +2 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +7 -15
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +9 -5
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SearchFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/TextFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useAsyncData.ts +11 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/hooks/useAsyncData.ts +67 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +2 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +7 -15
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +19 -25
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/org-setup.config.json +0 -1
- package/dist/scripts/org-setup.mjs +528 -44
- package/package.json +1 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/layout/card-skeleton.tsx +0 -38
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/authentication/layouts/authenticationRouteLayout.tsx +0 -21
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useCachedAsyncData.ts +0 -188
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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
|
+
## [3.1.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v3.1.0...v3.1.1) (2026-05-12)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [3.1.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v3.0.0...v3.1.0) (2026-05-12)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* post tdx changes @W-22390798 ([#500](https://github.com/salesforce-experience-platform-emu/webapps/issues/500)) ([b611d9e](https://github.com/salesforce-experience-platform-emu/webapps/commit/b611d9ecbadd07d052bd7d9a4f5659dec422bf22))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
## [3.0.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v2.2.1...v3.0.0) (2026-05-11)
|
|
7
24
|
|
|
8
25
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import { AlertCircleIcon, CheckCircle2Icon } from 'lucide-react';
|
|
2
|
+
import { AlertCircleIcon, CheckCircle2Icon, InfoIcon } from 'lucide-react';
|
|
3
3
|
import { Alert, AlertDescription } from '../../components/ui/alert';
|
|
4
4
|
import { useId } from 'react';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ const statusAlertVariants = cva('', {
|
|
|
8
8
|
variant: {
|
|
9
9
|
error: '',
|
|
10
10
|
success: '',
|
|
11
|
+
info: 'text-blue-600 *:[svg]:text-current *:data-[slot=alert-description]:text-blue-600/90',
|
|
11
12
|
},
|
|
12
13
|
},
|
|
13
14
|
defaultVariants: {
|
|
@@ -18,11 +19,11 @@ const statusAlertVariants = cva('', {
|
|
|
18
19
|
interface StatusAlertProps extends VariantProps<typeof statusAlertVariants> {
|
|
19
20
|
children?: React.ReactNode;
|
|
20
21
|
/** Alert variant type. @default "error" */
|
|
21
|
-
variant?: 'error' | 'success';
|
|
22
|
+
variant?: 'error' | 'success' | 'info';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Status alert component for displaying error or
|
|
26
|
+
* Status alert component for displaying error, success, or info messages.
|
|
26
27
|
* Returns null if no children are provided.
|
|
27
28
|
*/
|
|
28
29
|
export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
@@ -31,6 +32,12 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
31
32
|
|
|
32
33
|
const isError = variant === 'error';
|
|
33
34
|
|
|
35
|
+
const icon = {
|
|
36
|
+
error: <AlertCircleIcon aria-hidden="true" />,
|
|
37
|
+
success: <CheckCircle2Icon aria-hidden="true" />,
|
|
38
|
+
info: <InfoIcon aria-hidden="true" />,
|
|
39
|
+
}[variant];
|
|
40
|
+
|
|
34
41
|
return (
|
|
35
42
|
<Alert
|
|
36
43
|
variant={isError ? 'destructive' : 'default'}
|
|
@@ -38,11 +45,7 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
38
45
|
aria-describedby={descriptionId}
|
|
39
46
|
role={isError ? 'alert' : 'status'}
|
|
40
47
|
>
|
|
41
|
-
{
|
|
42
|
-
<AlertCircleIcon aria-hidden="true" />
|
|
43
|
-
) : (
|
|
44
|
-
<CheckCircle2Icon aria-hidden="true" />
|
|
45
|
-
)}
|
|
48
|
+
{icon}
|
|
46
49
|
<AlertDescription id={descriptionId}>{children}</AlertDescription>
|
|
47
50
|
</Alert>
|
|
48
51
|
);
|
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
|
8
8
|
type={type}
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
|
-
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
11
|
+
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground/70 placeholder:italic w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
@@ -48,7 +48,8 @@ function getUserProfileMutation(fields: string): string {
|
|
|
48
48
|
|
|
49
49
|
function throwOnGraphQLErrors(response: any): void {
|
|
50
50
|
if (response?.errors?.length) {
|
|
51
|
-
|
|
51
|
+
console.error("GraphQL request failed", response.errors);
|
|
52
|
+
throw new Error("An unexpected error occurred");
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -49,13 +49,13 @@ export const AUTH_REDIRECT_PARAM = "startUrl";
|
|
|
49
49
|
* Placeholder text constants for authentication form inputs.
|
|
50
50
|
*/
|
|
51
51
|
export const AUTH_PLACEHOLDERS = {
|
|
52
|
-
EMAIL: "
|
|
53
|
-
PASSWORD: "",
|
|
54
|
-
PASSWORD_CREATE: "",
|
|
55
|
-
PASSWORD_CONFIRM: "",
|
|
56
|
-
PASSWORD_NEW: "",
|
|
57
|
-
PASSWORD_NEW_CONFIRM: "",
|
|
58
|
-
FIRST_NAME: "
|
|
59
|
-
LAST_NAME: "
|
|
60
|
-
USERNAME: "
|
|
52
|
+
EMAIL: "e.g. name@example.com",
|
|
53
|
+
PASSWORD: "Enter your password",
|
|
54
|
+
PASSWORD_CREATE: "Create a password",
|
|
55
|
+
PASSWORD_CONFIRM: "Re-enter your password",
|
|
56
|
+
PASSWORD_NEW: "Enter new password",
|
|
57
|
+
PASSWORD_NEW_CONFIRM: "Re-enter new password",
|
|
58
|
+
FIRST_NAME: "e.g. Alex",
|
|
59
|
+
LAST_NAME: "e.g. Smith",
|
|
60
|
+
USERNAME: "e.g. asmith",
|
|
61
61
|
} as const;
|
|
@@ -35,8 +35,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
35
35
|
const userData = await getCurrentUser();
|
|
36
36
|
setUser(userData);
|
|
37
37
|
} catch (err) {
|
|
38
|
-
|
|
39
|
-
setError(
|
|
38
|
+
console.error("Authentication failed", err);
|
|
39
|
+
setError("Authentication failed");
|
|
40
40
|
setUser(null);
|
|
41
41
|
} finally {
|
|
42
42
|
setLoading(false);
|
|
@@ -53,8 +53,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
53
53
|
}, []);
|
|
54
54
|
|
|
55
55
|
useEffect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
getCurrentUser()
|
|
58
|
+
.then((userData) => {
|
|
59
|
+
if (!cancelled) setUser(userData);
|
|
60
|
+
})
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
console.error("Authentication failed", err);
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setError("Authentication failed");
|
|
65
|
+
setUser(null);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
if (!cancelled) setLoading(false);
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
58
75
|
|
|
59
76
|
const value: AuthContextType = {
|
|
60
77
|
user,
|
|
@@ -4,6 +4,7 @@ import { FooterLink } from "../footers/footer-link";
|
|
|
4
4
|
import { SubmitButton } from "./submit-button";
|
|
5
5
|
import { CardLayout } from "../../../components/layouts/card-layout";
|
|
6
6
|
import { useFormContext } from "../hooks/form";
|
|
7
|
+
import { useAuth } from "../context/AuthContext";
|
|
7
8
|
import { useId } from "react";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -15,6 +16,8 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
|
15
16
|
description: string;
|
|
16
17
|
error?: React.ReactNode;
|
|
17
18
|
success?: React.ReactNode;
|
|
19
|
+
/** Whether to show the "already logged in" alert and disable submit when authenticated. @default true */
|
|
20
|
+
showAlreadyLoggedIn?: boolean;
|
|
18
21
|
submit: {
|
|
19
22
|
text: string;
|
|
20
23
|
loadingText?: string;
|
|
@@ -32,6 +35,10 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
|
32
35
|
* Wraps the specific logic of Login/Register forms with a consistent visual frame (Card),
|
|
33
36
|
* title, and error alert placement. Extends form element props for flexibility.
|
|
34
37
|
* This ensures all auth-related pages look and behave similarly.
|
|
38
|
+
*
|
|
39
|
+
* Auth-aware behavior:
|
|
40
|
+
* - While auth state is loading, the submit button is disabled.
|
|
41
|
+
* - If the user is already authenticated, an info alert is shown and submit is disabled.
|
|
35
42
|
*/
|
|
36
43
|
export function AuthForm({
|
|
37
44
|
id: providedId,
|
|
@@ -39,18 +46,25 @@ export function AuthForm({
|
|
|
39
46
|
description,
|
|
40
47
|
error,
|
|
41
48
|
success,
|
|
49
|
+
showAlreadyLoggedIn = true,
|
|
42
50
|
children,
|
|
43
51
|
submit,
|
|
44
52
|
footer,
|
|
45
53
|
...props
|
|
46
54
|
}: AuthFormProps) {
|
|
47
55
|
const form = useFormContext();
|
|
56
|
+
const { isAuthenticated, loading } = useAuth();
|
|
48
57
|
const generatedId = useId();
|
|
49
58
|
const id = providedId ?? generatedId;
|
|
50
59
|
|
|
60
|
+
const showAuthAlert = showAlreadyLoggedIn && isAuthenticated;
|
|
61
|
+
const isSubmitDisabled = submit.disabled || showAuthAlert || loading;
|
|
62
|
+
|
|
51
63
|
return (
|
|
52
64
|
<CardLayout title={title} description={description}>
|
|
53
65
|
<div className="space-y-6">
|
|
66
|
+
{/* [Dev Note] Auth status alert for authenticated users on public pages */}
|
|
67
|
+
{showAuthAlert && <StatusAlert variant="info">You are already logged in.</StatusAlert>}
|
|
54
68
|
{/* [Dev Note] Global form error alert (e.g. "Invalid Credentials") */}
|
|
55
69
|
{error && <StatusAlert variant="error">{error}</StatusAlert>}
|
|
56
70
|
{success && <StatusAlert variant="success">{success}</StatusAlert>}
|
|
@@ -69,7 +83,7 @@ export function AuthForm({
|
|
|
69
83
|
form={id}
|
|
70
84
|
label={submit.text}
|
|
71
85
|
loadingLabel={submit.loadingText}
|
|
72
|
-
disabled={
|
|
86
|
+
disabled={isSubmitDisabled}
|
|
73
87
|
className="mt-6"
|
|
74
88
|
/>
|
|
75
89
|
</form>
|
|
@@ -40,7 +40,7 @@ function TextField({
|
|
|
40
40
|
const id = providedId ?? generatedId;
|
|
41
41
|
const descriptionId = `${id}-description`;
|
|
42
42
|
const errorId = `${id}-error`;
|
|
43
|
-
const isInvalid = field.state.meta.
|
|
43
|
+
const isInvalid = field.state.meta.isBlurred && field.state.meta.errors.length > 0;
|
|
44
44
|
|
|
45
45
|
// Deduplicate errors by message
|
|
46
46
|
const errors = field.state.meta.errors;
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { Navigate, Outlet, useLocation } from "react-router";
|
|
2
2
|
import { useAuth } from "../context/AuthContext";
|
|
3
3
|
import { AUTH_REDIRECT_PARAM, ROUTES } from "../authenticationConfig";
|
|
4
|
-
import { CardSkeleton } from "../layout/card-skeleton";
|
|
5
|
-
|
|
6
|
-
export interface PrivateRouteProps {
|
|
7
|
-
/**
|
|
8
|
-
* Whether to show a card skeleton placeholder while authentication is loading.
|
|
9
|
-
* @default false
|
|
10
|
-
*/
|
|
11
|
-
showCardSkeleton?: boolean;
|
|
12
|
-
}
|
|
13
4
|
|
|
14
5
|
/**
|
|
15
6
|
* [Dev Note] Route Guard:
|
|
@@ -17,11 +8,11 @@ export interface PrivateRouteProps {
|
|
|
17
8
|
* Otherwise, redirects to Login with a 'startUrl' parameter so the user can be
|
|
18
9
|
* returned to this page after successful login.
|
|
19
10
|
*/
|
|
20
|
-
export default function PrivateRoute(
|
|
11
|
+
export default function PrivateRoute() {
|
|
21
12
|
const { isAuthenticated, loading } = useAuth();
|
|
22
13
|
const location = useLocation();
|
|
23
14
|
|
|
24
|
-
if (loading) return
|
|
15
|
+
if (loading) return null;
|
|
25
16
|
|
|
26
17
|
if (!isAuthenticated) {
|
|
27
18
|
const searchParams = new URLSearchParams();
|
|
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
7
7
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
8
8
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
9
9
|
import { newPasswordSchema } from "../authHelpers";
|
|
10
|
-
import {
|
|
10
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
11
11
|
|
|
12
12
|
const changePasswordSchema = z
|
|
13
13
|
.object({
|
|
@@ -17,7 +17,7 @@ const changePasswordSchema = z
|
|
|
17
17
|
|
|
18
18
|
export default function ChangePassword() {
|
|
19
19
|
const [success, setSuccess] = useState(false);
|
|
20
|
-
const [submitError, setSubmitError] = useState<
|
|
20
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
21
21
|
|
|
22
22
|
const form = useAppForm({
|
|
23
23
|
defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" },
|
|
@@ -40,11 +40,26 @@ export default function ChangePassword() {
|
|
|
40
40
|
Accept: "application/json",
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
|
-
await handleApiResponse(response
|
|
43
|
+
await handleApiResponse(response);
|
|
44
44
|
setSuccess(true);
|
|
45
45
|
form.reset();
|
|
46
46
|
} catch (err) {
|
|
47
|
-
|
|
47
|
+
console.error("Password change failed", err);
|
|
48
|
+
if (err instanceof ApiError) {
|
|
49
|
+
setSubmitError(
|
|
50
|
+
err.errors.length === 1 ? (
|
|
51
|
+
err.errors[0]
|
|
52
|
+
) : (
|
|
53
|
+
<ul>
|
|
54
|
+
{err.errors.map((e, i) => (
|
|
55
|
+
<li key={i}>{e}</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
setSubmitError("Password change failed");
|
|
62
|
+
}
|
|
48
63
|
}
|
|
49
64
|
},
|
|
50
65
|
onSubmitInvalid: () => {},
|
|
@@ -56,6 +71,7 @@ export default function ChangePassword() {
|
|
|
56
71
|
<AuthForm
|
|
57
72
|
title="Change Password"
|
|
58
73
|
description="Enter your current and new password below"
|
|
74
|
+
showAlreadyLoggedIn={false}
|
|
59
75
|
error={submitError}
|
|
60
76
|
success={
|
|
61
77
|
success && (
|
|
@@ -5,7 +5,7 @@ import { AuthForm } from "../forms/auth-form";
|
|
|
5
5
|
import { useAppForm } from "../hooks/form";
|
|
6
6
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
7
7
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
8
|
-
import {
|
|
8
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
9
9
|
|
|
10
10
|
const forgotPasswordSchema = z.object({
|
|
11
11
|
username: z.string().trim().toLowerCase().email("Please enter a valid username"),
|
|
@@ -13,7 +13,7 @@ const forgotPasswordSchema = z.object({
|
|
|
13
13
|
|
|
14
14
|
export default function ForgotPassword() {
|
|
15
15
|
const [success, setSuccess] = useState(false);
|
|
16
|
-
const [submitError, setSubmitError] = useState<
|
|
16
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
17
17
|
|
|
18
18
|
const form = useAppForm({
|
|
19
19
|
defaultValues: { username: "" },
|
|
@@ -33,10 +33,25 @@ export default function ForgotPassword() {
|
|
|
33
33
|
Accept: "application/json",
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
|
-
await handleApiResponse(response
|
|
36
|
+
await handleApiResponse(response);
|
|
37
37
|
setSuccess(true);
|
|
38
38
|
} catch (err) {
|
|
39
|
-
|
|
39
|
+
console.error("Failed to send reset link", err);
|
|
40
|
+
if (err instanceof ApiError) {
|
|
41
|
+
setSubmitError(
|
|
42
|
+
err.errors.length === 1 ? (
|
|
43
|
+
err.errors[0]
|
|
44
|
+
) : (
|
|
45
|
+
<ul>
|
|
46
|
+
{err.errors.map((e, i) => (
|
|
47
|
+
<li key={i}>{e}</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
setSubmitError("Failed to send reset link");
|
|
54
|
+
}
|
|
40
55
|
}
|
|
41
56
|
},
|
|
42
57
|
onSubmitInvalid: () => {},
|
|
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
7
7
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
8
8
|
import { ROUTES } from "../authenticationConfig";
|
|
9
9
|
import { emailSchema, getStartUrl, type AuthResponse } from "../authHelpers";
|
|
10
|
-
import {
|
|
10
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
11
11
|
|
|
12
12
|
const loginSchema = z.object({
|
|
13
13
|
email: emailSchema,
|
|
@@ -17,7 +17,7 @@ const loginSchema = z.object({
|
|
|
17
17
|
export default function Login() {
|
|
18
18
|
const navigate = useNavigate();
|
|
19
19
|
const [searchParams] = useSearchParams();
|
|
20
|
-
const [submitError, setSubmitError] = useState<
|
|
20
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
21
21
|
|
|
22
22
|
const form = useAppForm({
|
|
23
23
|
defaultValues: { email: "", password: "" },
|
|
@@ -43,7 +43,7 @@ export default function Login() {
|
|
|
43
43
|
Accept: "application/json",
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
|
-
const result = await handleApiResponse<AuthResponse>(response
|
|
46
|
+
const result = await handleApiResponse<AuthResponse>(response);
|
|
47
47
|
if (result?.redirectUrl) {
|
|
48
48
|
// Hard navigate to the URL which establishes the server session cookie
|
|
49
49
|
window.location.replace(result.redirectUrl);
|
|
@@ -52,7 +52,22 @@ export default function Login() {
|
|
|
52
52
|
navigate("/", { replace: true });
|
|
53
53
|
}
|
|
54
54
|
} catch (err) {
|
|
55
|
-
|
|
55
|
+
console.error("Login failed", err);
|
|
56
|
+
if (err instanceof ApiError) {
|
|
57
|
+
setSubmitError(
|
|
58
|
+
err.errors.length === 1 ? (
|
|
59
|
+
err.errors[0]
|
|
60
|
+
) : (
|
|
61
|
+
<ul>
|
|
62
|
+
{err.errors.map((e, i) => (
|
|
63
|
+
<li key={i}>{e}</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
setSubmitError("Login failed");
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
},
|
|
58
73
|
onSubmitInvalid: () => {},
|
|
@@ -2,14 +2,14 @@ import { useState, useEffect } from "react";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
|
-
import { CardSkeleton } from "../layout/card-skeleton";
|
|
6
5
|
import { AuthForm } from "../forms/auth-form";
|
|
7
6
|
import { useAppForm } from "../hooks/form";
|
|
8
7
|
import { ROUTES } from "../authenticationConfig";
|
|
9
8
|
import { emailSchema } from "../authHelpers";
|
|
10
|
-
import { getErrorMessage } from "../utils/helpers";
|
|
11
9
|
import { useUser } from "../context/AuthContext";
|
|
12
10
|
import { fetchUserProfile, updateUserProfile } from "../api/userProfileApi";
|
|
11
|
+
import { Skeleton } from "../../../components/ui/skeleton";
|
|
12
|
+
import { Field, FieldLabel } from "../../../components/ui/field";
|
|
13
13
|
|
|
14
14
|
const optionalString = z
|
|
15
15
|
.string()
|
|
@@ -32,6 +32,38 @@ const profileSchema = z.object({
|
|
|
32
32
|
|
|
33
33
|
type ProfileFormValues = z.infer<typeof profileSchema>;
|
|
34
34
|
|
|
35
|
+
function FieldSkeleton({ label }: { label: string }) {
|
|
36
|
+
return (
|
|
37
|
+
<Field>
|
|
38
|
+
<FieldLabel>{label}</FieldLabel>
|
|
39
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
40
|
+
</Field>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ProfileFieldsSkeleton() {
|
|
45
|
+
return (
|
|
46
|
+
<div role="status" aria-live="polite">
|
|
47
|
+
<span className="sr-only">Loading profile…</span>
|
|
48
|
+
<FieldSkeleton label="Email" />
|
|
49
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
50
|
+
<FieldSkeleton label="First name" />
|
|
51
|
+
<FieldSkeleton label="Last name" />
|
|
52
|
+
</div>
|
|
53
|
+
<FieldSkeleton label="Phone" />
|
|
54
|
+
<FieldSkeleton label="Street" />
|
|
55
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
56
|
+
<FieldSkeleton label="City" />
|
|
57
|
+
<FieldSkeleton label="State" />
|
|
58
|
+
</div>
|
|
59
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
60
|
+
<FieldSkeleton label="Postal Code" />
|
|
61
|
+
<FieldSkeleton label="Country" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
export default function Profile() {
|
|
36
68
|
const user = useUser();
|
|
37
69
|
const [profile, setProfile] = useState<ProfileFormValues | null>(null);
|
|
@@ -62,7 +94,8 @@ export default function Profile() {
|
|
|
62
94
|
setSuccess(true);
|
|
63
95
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
64
96
|
} catch (err) {
|
|
65
|
-
|
|
97
|
+
console.error("Failed to update profile", err);
|
|
98
|
+
setSubmitError("Failed to update profile");
|
|
66
99
|
}
|
|
67
100
|
},
|
|
68
101
|
onSubmitInvalid: () => {},
|
|
@@ -88,10 +121,9 @@ export default function Profile() {
|
|
|
88
121
|
}
|
|
89
122
|
})
|
|
90
123
|
.catch((err: any) => {
|
|
124
|
+
console.error("Failed to load profile", err);
|
|
91
125
|
if (mounted) {
|
|
92
|
-
setLoadError(
|
|
93
|
-
} else {
|
|
94
|
-
console.error("Failed to load profile", err);
|
|
126
|
+
setLoadError("Failed to load profile");
|
|
95
127
|
}
|
|
96
128
|
});
|
|
97
129
|
return () => {
|
|
@@ -106,9 +138,7 @@ export default function Profile() {
|
|
|
106
138
|
}
|
|
107
139
|
}, [profile, form]);
|
|
108
140
|
|
|
109
|
-
|
|
110
|
-
return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
|
|
111
|
-
}
|
|
141
|
+
const loading = !profile && !loadError;
|
|
112
142
|
|
|
113
143
|
return (
|
|
114
144
|
<CenteredPageLayout contentMaxWidth="md" title={ROUTES.PROFILE.TITLE}>
|
|
@@ -116,44 +146,51 @@ export default function Profile() {
|
|
|
116
146
|
<AuthForm
|
|
117
147
|
title="Profile"
|
|
118
148
|
description="Update your account information"
|
|
149
|
+
showAlreadyLoggedIn={false}
|
|
119
150
|
error={loadError ?? submitError}
|
|
120
151
|
success={success && "Profile updated!"}
|
|
121
|
-
submit={{ text: "Save Changes", loadingText: "Saving…" }}
|
|
152
|
+
submit={{ text: "Save Changes", loadingText: "Saving…", disabled: loading }}
|
|
122
153
|
footer={{ link: ROUTES.CHANGE_PASSWORD.PATH, linkText: "Change password" }}
|
|
123
154
|
>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
{loading ? (
|
|
156
|
+
<ProfileFieldsSkeleton />
|
|
157
|
+
) : (
|
|
158
|
+
<>
|
|
159
|
+
<form.AppField name="Email">
|
|
160
|
+
{(field) => <field.EmailField label="Email" disabled />}
|
|
161
|
+
</form.AppField>
|
|
162
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
163
|
+
<form.AppField name="FirstName">
|
|
164
|
+
{(field) => <field.TextField label="First name" autoComplete="given-name" />}
|
|
165
|
+
</form.AppField>
|
|
166
|
+
<form.AppField name="LastName">
|
|
167
|
+
{(field) => <field.TextField label="Last name" autoComplete="family-name" />}
|
|
168
|
+
</form.AppField>
|
|
169
|
+
</div>
|
|
170
|
+
<form.AppField name="Phone">
|
|
171
|
+
{(field) => <field.TextField label="Phone" type="tel" autoComplete="tel" />}
|
|
172
|
+
</form.AppField>
|
|
173
|
+
<form.AppField name="Street">
|
|
174
|
+
{(field) => <field.TextField label="Street" autoComplete="street-address" />}
|
|
175
|
+
</form.AppField>
|
|
176
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
177
|
+
<form.AppField name="City">
|
|
178
|
+
{(field) => <field.TextField label="City" autoComplete="address-level2" />}
|
|
179
|
+
</form.AppField>
|
|
180
|
+
<form.AppField name="State">
|
|
181
|
+
{(field) => <field.TextField label="State" autoComplete="address-level1" />}
|
|
182
|
+
</form.AppField>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
185
|
+
<form.AppField name="PostalCode">
|
|
186
|
+
{(field) => <field.TextField label="Postal Code" autoComplete="postal-code" />}
|
|
187
|
+
</form.AppField>
|
|
188
|
+
<form.AppField name="Country">
|
|
189
|
+
{(field) => <field.TextField label="Country" autoComplete="country-name" />}
|
|
190
|
+
</form.AppField>
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
157
194
|
</AuthForm>
|
|
158
195
|
</form.AppForm>
|
|
159
196
|
</CenteredPageLayout>
|
|
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
7
7
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
8
8
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
9
9
|
import { emailSchema, passwordSchema, getStartUrl, type AuthResponse } from "../authHelpers";
|
|
10
|
-
import {
|
|
10
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
11
11
|
|
|
12
12
|
const registerSchema = z
|
|
13
13
|
.object({
|
|
@@ -26,7 +26,7 @@ const registerSchema = z
|
|
|
26
26
|
export default function Register() {
|
|
27
27
|
const navigate = useNavigate();
|
|
28
28
|
const [searchParams] = useSearchParams();
|
|
29
|
-
const [submitError, setSubmitError] = useState<
|
|
29
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
30
30
|
|
|
31
31
|
const form = useAppForm({
|
|
32
32
|
defaultValues: {
|
|
@@ -56,7 +56,7 @@ export default function Register() {
|
|
|
56
56
|
Accept: "application/json",
|
|
57
57
|
},
|
|
58
58
|
});
|
|
59
|
-
const result = await handleApiResponse<AuthResponse>(response
|
|
59
|
+
const result = await handleApiResponse<AuthResponse>(response);
|
|
60
60
|
if (result?.redirectUrl) {
|
|
61
61
|
// Hard navigate to the URL which logs the new user in
|
|
62
62
|
window.location.replace(result.redirectUrl);
|
|
@@ -65,7 +65,18 @@ export default function Register() {
|
|
|
65
65
|
navigate(ROUTES.LOGIN.PATH, { replace: true });
|
|
66
66
|
}
|
|
67
67
|
} catch (err) {
|
|
68
|
-
|
|
68
|
+
console.error("Registration failed", err);
|
|
69
|
+
if (err instanceof ApiError) {
|
|
70
|
+
setSubmitError(
|
|
71
|
+
<ul>
|
|
72
|
+
{err.errors.map((e, i) => (
|
|
73
|
+
<li key={i}>{e}</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>,
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
setSubmitError("Registration failed");
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
},
|
|
71
82
|
onSubmitInvalid: () => {},
|