@salesforce/templates 66.7.13 → 66.9.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/lib/generators/lightningEmbeddingGenerator.d.ts +6 -0
- package/lib/generators/lightningEmbeddingGenerator.js +68 -0
- package/lib/generators/lightningEmbeddingGenerator.js.map +1 -0
- package/lib/i18n/i18n.d.ts +6 -0
- package/lib/i18n/i18n.js +6 -0
- package/lib/i18n/i18n.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/templates/lightningembedding/default/default.css +5 -0
- package/lib/templates/lightningembedding/default/default.html +7 -0
- package/lib/templates/lightningembedding/default/default.js +5 -0
- package/lib/templates/lightningembedding/default/default.js-meta.xml +12 -0
- package/lib/templates/project/reactexternalapp/AGENT.md +3 -3
- package/lib/templates/project/reactexternalapp/CHANGELOG.md +428 -0
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +792 -2031
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/api/userProfileApi.ts +12 -11
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/authenticationConfig.ts +9 -9
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/context/AuthContext.tsx +21 -4
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/forms/auth-form.tsx +15 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/form.tsx +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/privateRouteLayout.tsx +2 -11
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ChangePassword.tsx +21 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ForgotPassword.tsx +20 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +20 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Profile.tsx +80 -43
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +16 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ResetPassword.tsx +20 -5
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/utils/helpers.ts +15 -52
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +19 -25
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
- package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleAuthUtils.cls-meta.xml +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleChangePassword.cls-meta.xml +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleForgotPassword.cls-meta.xml +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleLogin.cls-meta.xml +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleRegistration.cls-meta.xml +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/package.xml +1 -1
- package/lib/templates/project/reactexternalapp/package.json +1 -1
- package/lib/templates/project/reactexternalapp/scripts/org-setup.config.json +0 -1
- package/lib/templates/project/reactexternalapp/scripts/org-setup.mjs +528 -44
- package/lib/templates/project/reactexternalapp/sfdx-project.json +1 -1
- package/lib/templates/project/reactinternalapp/AGENT.md +3 -3
- package/lib/templates/project/reactinternalapp/CHANGELOG.md +428 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +784 -2036
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +5 -5
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.uibundle-meta.xml +1 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +40 -44
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +9 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/globals.d.ts +13 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
- package/lib/templates/project/reactinternalapp/_p_/_m_/applications/reactinternalapp.app-meta.xml +17 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/permissionsets/reactinternalapp_Access.permissionset-meta.xml +9 -0
- package/lib/templates/project/reactinternalapp/package.json +1 -1
- package/lib/templates/project/reactinternalapp/scripts/org-setup.config.json +6 -3
- package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +528 -44
- package/lib/templates/project/reactinternalapp/sfdx-project.json +1 -1
- package/lib/templates/uiBundles/reactbasic/package-lock.json +1040 -593
- package/lib/templates/uiBundles/reactbasic/package.json +3 -3
- package/lib/templates/uiBundles/reactbasic/src/api/graphqlClient.ts +13 -13
- package/lib/templates/uiBundles/reactbasic/src/components/alerts/status-alert.tsx +11 -8
- package/lib/templates/uiBundles/reactbasic/src/components/ui/input.tsx +1 -1
- package/lib/templates/uiBundles/reactbasic/src/hooks/useAsyncData.ts +67 -0
- package/lib/templates/uiBundles/reactbasic/tsconfig.json +4 -6
- package/lib/templates/uiBundles/reactbasic/vite-env.d.ts +0 -3
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/lightningEmbedding.d.ts +12 -0
- package/lib/utils/lightningEmbedding.js +50 -0
- package/lib/utils/lightningEmbedding.js.map +1 -0
- package/lib/utils/types.d.ts +15 -6
- package/lib/utils/types.js +8 -5
- package/lib/utils/types.js.map +1 -1
- package/package.json +6 -6
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layout/card-skeleton.tsx +0 -38
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/authenticationRouteLayout.tsx +0 -21
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
|
@@ -4,10 +4,10 @@ import { z } from "zod";
|
|
|
4
4
|
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
5
|
import { AuthForm } from "../forms/auth-form";
|
|
6
6
|
import { useAppForm } from "../hooks/form";
|
|
7
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
7
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
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>
|
|
@@ -4,10 +4,10 @@ import { z } from "zod";
|
|
|
4
4
|
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
5
|
import { AuthForm } from "../forms/auth-form";
|
|
6
6
|
import { useAppForm } from "../hooks/form";
|
|
7
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
7
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
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: () => {},
|
|
@@ -5,16 +5,16 @@ import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
|
5
5
|
import { AuthForm } from "../forms/auth-form";
|
|
6
6
|
import { StatusAlert } from "../../../components/alerts/status-alert";
|
|
7
7
|
import { useAppForm } from "../hooks/form";
|
|
8
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
8
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
9
9
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
10
10
|
import { newPasswordSchema } from "../authHelpers";
|
|
11
|
-
import {
|
|
11
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
12
12
|
|
|
13
13
|
export default function ResetPassword() {
|
|
14
14
|
const [searchParams] = useSearchParams();
|
|
15
15
|
const token = searchParams.get("token");
|
|
16
16
|
const [success, setSuccess] = useState(false);
|
|
17
|
-
const [submitError, setSubmitError] = useState<
|
|
17
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
18
18
|
|
|
19
19
|
const form = useAppForm({
|
|
20
20
|
defaultValues: { newPassword: "", confirmPassword: "" },
|
|
@@ -34,12 +34,27 @@ export default function ResetPassword() {
|
|
|
34
34
|
Accept: "application/json",
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
|
-
await handleApiResponse(response
|
|
37
|
+
await handleApiResponse(response);
|
|
38
38
|
setSuccess(true);
|
|
39
39
|
// Scroll to top of page after successful submission so user sees it
|
|
40
40
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
41
41
|
} catch (err) {
|
|
42
|
-
|
|
42
|
+
console.error("Password reset failed", err);
|
|
43
|
+
if (err instanceof ApiError) {
|
|
44
|
+
setSubmitError(
|
|
45
|
+
err.errors.length === 1 ? (
|
|
46
|
+
err.errors[0]
|
|
47
|
+
) : (
|
|
48
|
+
<ul>
|
|
49
|
+
{err.errors.map((e, i) => (
|
|
50
|
+
<li key={i}>{e}</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
setSubmitError("Password reset failed");
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
},
|
|
45
60
|
onSubmitInvalid: () => {},
|
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @param err - The error object (unknown type)
|
|
6
|
-
* @param fallback - Fallback message if error doesn't have a message property
|
|
7
|
-
* @returns The error message string
|
|
2
|
+
* Error thrown when the API returns a non-OK response with structured error messages.
|
|
3
|
+
* The `errors` array contains user-facing messages that are safe to display —
|
|
4
|
+
* backend Apex classes guarantee that system exceptions are never exposed.
|
|
8
5
|
*/
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
export class ApiError extends Error {
|
|
7
|
+
errors: string[];
|
|
8
|
+
constructor(errors: string[]) {
|
|
9
|
+
super(errors[0]);
|
|
10
|
+
this.name = "ApiError";
|
|
11
|
+
this.errors = errors;
|
|
15
12
|
}
|
|
16
|
-
return fallback;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
/**
|
|
20
16
|
* [Dev Note] Helper to parse the fetch Response.
|
|
21
17
|
* It handles the distinction between success (JSON) and failure (throwing Error).
|
|
22
18
|
*/
|
|
23
|
-
export async function handleApiResponse<T = unknown>(
|
|
24
|
-
response: Response,
|
|
25
|
-
fallbackError: string,
|
|
26
|
-
): Promise<T> {
|
|
19
|
+
export async function handleApiResponse<T = unknown>(response: Response): Promise<T> {
|
|
27
20
|
// 1. Robustness: Handle 204 No Content gracefully
|
|
28
21
|
if (response.status === 204) {
|
|
29
22
|
return {} as T;
|
|
@@ -41,9 +34,11 @@ export async function handleApiResponse<T = unknown>(
|
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
if (!response.ok) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
console.error("API request failed", data);
|
|
38
|
+
if (data?.errors?.length) {
|
|
39
|
+
throw new ApiError(data.errors);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("An unexpected error occurred");
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
return data as T;
|
|
@@ -87,35 +82,3 @@ export function flattenGraphQLRecord<T>(
|
|
|
87
82
|
]),
|
|
88
83
|
) as T;
|
|
89
84
|
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* [Dev Note] Salesforce APIs may return errors as an array or a single object.
|
|
93
|
-
* This helper standardizes the extraction of the error message string.
|
|
94
|
-
*
|
|
95
|
-
* @param data - The response data.
|
|
96
|
-
* @param fallbackError - Fallback error message if response doesn't have a message property
|
|
97
|
-
* @returns The error message string
|
|
98
|
-
*/
|
|
99
|
-
function parseApiResponseError(
|
|
100
|
-
data: any,
|
|
101
|
-
fallbackError: string = "An unknown error occurred",
|
|
102
|
-
): string {
|
|
103
|
-
if (data?.message) {
|
|
104
|
-
return data.message;
|
|
105
|
-
}
|
|
106
|
-
if (data?.error) {
|
|
107
|
-
return data.error;
|
|
108
|
-
}
|
|
109
|
-
if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0) {
|
|
110
|
-
return data.errors.join(" ") || fallbackError;
|
|
111
|
-
}
|
|
112
|
-
if (Array.isArray(data) && data.length > 0) {
|
|
113
|
-
return (
|
|
114
|
-
data
|
|
115
|
-
.map((e) => e?.message)
|
|
116
|
-
.filter(Boolean)
|
|
117
|
-
.join(" ") || fallbackError
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
return fallbackError;
|
|
121
|
-
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thin GraphQL client: createDataSDK +
|
|
3
|
-
* Use with gql-tagged queries and generated operation types
|
|
2
|
+
* Thin GraphQL client: createDataSDK + sdk.graphql.query with centralized
|
|
3
|
+
* error handling. Use with gql-tagged queries and generated operation types
|
|
4
|
+
* for type-safe calls.
|
|
4
5
|
*/
|
|
5
|
-
import { createDataSDK } from '@salesforce/sdk
|
|
6
|
+
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
6
7
|
|
|
7
8
|
export async function executeGraphQL<TData, TVariables>(
|
|
8
9
|
query: string,
|
|
9
10
|
variables?: TVariables
|
|
10
11
|
): Promise<TData> {
|
|
11
12
|
const data = await createDataSDK();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
variables,
|
|
13
|
+
const result = await data.graphql!.query<TData, TVariables>({
|
|
14
|
+
query: query,
|
|
15
|
+
variables: variables,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
18
|
+
if (result.errors?.length) {
|
|
19
|
+
const msg = result.errors.map(e => e.message).join('; ');
|
|
20
|
+
throw new Error(`GraphQL Error: ${msg}`);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
throw new Error(`GraphQL Error: ${msg}`);
|
|
23
|
+
if (result.data == null) {
|
|
24
|
+
throw new Error('GraphQL response data is null');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
return
|
|
27
|
+
return result.data;
|
|
28
28
|
}
|
|
@@ -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}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseAsyncDataResult<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs an async fetcher on mount and whenever `deps` change.
|
|
11
|
+
* Returns the loading/error/data state. Does not cache — every call
|
|
12
|
+
* to the fetcher hits the source directly.
|
|
13
|
+
*
|
|
14
|
+
* A cleanup flag prevents state updates if the component unmounts
|
|
15
|
+
* or deps change before the fetch completes (avoids React warnings
|
|
16
|
+
* and stale updates from out-of-order responses).
|
|
17
|
+
*/
|
|
18
|
+
export function useAsyncData<T>(
|
|
19
|
+
fetcher: () => Promise<T>,
|
|
20
|
+
deps: React.DependencyList
|
|
21
|
+
): UseAsyncDataResult<T> {
|
|
22
|
+
const [data, setData] = useState<T | null>(null);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [generation, setGeneration] = useState(0);
|
|
26
|
+
|
|
27
|
+
const fetcherRef = useRef(fetcher);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetcherRef.current = fetcher;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Detect dep changes during render to reset loading state and bump generation
|
|
33
|
+
const [prevDeps, setPrevDeps] = useState(deps);
|
|
34
|
+
if (
|
|
35
|
+
deps.length !== prevDeps.length ||
|
|
36
|
+
deps.some((d, i) => d !== prevDeps[i])
|
|
37
|
+
) {
|
|
38
|
+
setPrevDeps(deps);
|
|
39
|
+
setGeneration(g => g + 1);
|
|
40
|
+
if (!loading) setLoading(true);
|
|
41
|
+
if (error !== null) setError(null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
|
|
47
|
+
fetcherRef
|
|
48
|
+
.current()
|
|
49
|
+
.then(result => {
|
|
50
|
+
if (!cancelled) setData(result);
|
|
51
|
+
})
|
|
52
|
+
.catch(err => {
|
|
53
|
+
console.error(err);
|
|
54
|
+
if (!cancelled)
|
|
55
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (!cancelled) setLoading(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [generation]);
|
|
65
|
+
|
|
66
|
+
return { data, loading, error };
|
|
67
|
+
}
|
package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useParams, useNavigate } from "react-router";
|
|
3
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
3
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
4
4
|
import { AlertCircle, ChevronDown, ChevronRight, FileQuestion } from "lucide-react";
|
|
5
5
|
import GET_ACCOUNT_DETAIL from "../api/account/query/getAccountDetail.graphql?raw";
|
|
6
6
|
import type {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "../components/ui/collapsible";
|
|
23
23
|
import { Separator } from "../components/ui/separator";
|
|
24
24
|
import { Skeleton } from "../components/ui/skeleton";
|
|
25
|
-
import {
|
|
25
|
+
import { useAsyncData } from "../hooks/useAsyncData";
|
|
26
26
|
import { ObjectBreadcrumb } from "../features/object-search/components/ObjectBreadcrumb";
|
|
27
27
|
|
|
28
28
|
type AccountNode = NonNullable<
|
|
@@ -33,16 +33,16 @@ type AccountNode = NonNullable<
|
|
|
33
33
|
|
|
34
34
|
async function fetchAccountDetail(recordId: string): Promise<AccountNode | null | undefined> {
|
|
35
35
|
const data = await createDataSDK();
|
|
36
|
-
const
|
|
36
|
+
const result = await data.graphql!.query<GetAccountDetailQuery, GetAccountDetailQueryVariables>({
|
|
37
37
|
query: GET_ACCOUNT_DETAIL,
|
|
38
38
|
variables: { id: recordId },
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
if (
|
|
42
|
-
throw new Error(
|
|
41
|
+
if (result.errors?.length) {
|
|
42
|
+
throw new Error(result.errors.map((e) => e.message).join("; "));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
return
|
|
45
|
+
return result.data?.uiapi?.query?.Account?.edges?.[0]?.node;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export default function AccountObjectDetail() {
|
|
@@ -53,9 +53,7 @@ export default function AccountObjectDetail() {
|
|
|
53
53
|
data: account,
|
|
54
54
|
loading,
|
|
55
55
|
error,
|
|
56
|
-
} =
|
|
57
|
-
key: `account:${recordId}`,
|
|
58
|
-
});
|
|
56
|
+
} = useAsyncData(() => fetchAccountDetail(recordId!), [recordId]);
|
|
59
57
|
|
|
60
58
|
return (
|
|
61
59
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|