@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
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@salesforce/sdk
|
|
22
|
-
"@salesforce/ui-bundle": "
|
|
21
|
+
"@salesforce/platform-sdk": "file:../../../../../../../../../sdk/platform-sdk",
|
|
22
|
+
"@salesforce/ui-bundle": "file:../../../../../../../../../ui-bundle",
|
|
23
23
|
"@tailwindcss/vite": "^4.1.17",
|
|
24
|
+
"@tanstack/react-form": "^1.27.7",
|
|
24
25
|
"class-variance-authority": "^0.7.1",
|
|
25
26
|
"clsx": "^2.1.1",
|
|
26
27
|
"date-fns": "^4.1.0",
|
|
@@ -35,7 +36,6 @@
|
|
|
35
36
|
"tailwind-merge": "^3.5.0",
|
|
36
37
|
"tailwindcss": "^4.1.17",
|
|
37
38
|
"tw-animate-css": "^1.4.0",
|
|
38
|
-
"@tanstack/react-form": "^1.27.7",
|
|
39
39
|
"zod": "^4.3.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
47
47
|
"@graphql-tools/utils": "^11.0.0",
|
|
48
48
|
"@playwright/test": "^1.49.0",
|
|
49
|
-
"@salesforce/vite-plugin-ui-bundle": "
|
|
49
|
+
"@salesforce/vite-plugin-ui-bundle": "file:../../../../../../../../../vite-plugin-ui-bundle",
|
|
50
50
|
"@testing-library/jest-dom": "^6.6.3",
|
|
51
51
|
"@testing-library/react": "^16.1.0",
|
|
52
52
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -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/query/getAccountDetail.graphql?raw";
|
|
6
6
|
import type {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "../../../../components/ui/collapsible";
|
|
19
19
|
import { Separator } from "../../../../components/ui/separator";
|
|
20
20
|
import { Skeleton } from "../../../../components/ui/skeleton";
|
|
21
|
-
import {
|
|
21
|
+
import { useAsyncData } from "../../hooks/useAsyncData";
|
|
22
22
|
import { ObjectBreadcrumb } from "../../components/ObjectBreadcrumb";
|
|
23
23
|
|
|
24
24
|
type AccountNode = NonNullable<
|
|
@@ -29,16 +29,16 @@ type AccountNode = NonNullable<
|
|
|
29
29
|
|
|
30
30
|
async function fetchAccountDetail(recordId: string): Promise<AccountNode | null | undefined> {
|
|
31
31
|
const data = await createDataSDK();
|
|
32
|
-
const
|
|
32
|
+
const result = await data.graphql!.query<GetAccountDetailQuery, GetAccountDetailQueryVariables>({
|
|
33
33
|
query: GET_ACCOUNT_DETAIL,
|
|
34
34
|
variables: { id: recordId },
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
if (
|
|
38
|
-
throw new Error(
|
|
37
|
+
if (result.errors?.length) {
|
|
38
|
+
throw new Error(result.errors.map((e) => e.message).join("; "));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
return
|
|
41
|
+
return result.data?.uiapi?.query?.Account?.edges?.[0]?.node;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export default function AccountObjectDetail() {
|
|
@@ -49,9 +49,7 @@ export default function AccountObjectDetail() {
|
|
|
49
49
|
data: account,
|
|
50
50
|
loading,
|
|
51
51
|
error,
|
|
52
|
-
} =
|
|
53
|
-
key: `account:${recordId}`,
|
|
54
|
-
});
|
|
52
|
+
} = useAsyncData(() => fetchAccountDetail(recordId!), [recordId]);
|
|
55
53
|
|
|
56
54
|
return (
|
|
57
55
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
fetchDistinctIndustries,
|
|
7
7
|
fetchDistinctTypes,
|
|
8
8
|
} from "../api/accountSearchService";
|
|
9
|
-
import {
|
|
9
|
+
import { useAsyncData } from "../../hooks/useAsyncData";
|
|
10
10
|
import { fieldValue } from "../../utils/fieldUtils";
|
|
11
11
|
import { useObjectSearchParams } from "../../hooks/useObjectSearchParams";
|
|
12
12
|
import { Alert, AlertTitle, AlertDescription } from "../../../../components/ui/alert";
|
|
@@ -42,8 +42,8 @@ import PaginationControls from "../../components/PaginationControls";
|
|
|
42
42
|
import type { PaginationConfig } from "../../hooks/useObjectSearchParams";
|
|
43
43
|
|
|
44
44
|
const PAGINATION_CONFIG: PaginationConfig = {
|
|
45
|
-
defaultPageSize:
|
|
46
|
-
validPageSizes: [
|
|
45
|
+
defaultPageSize: 7,
|
|
46
|
+
validPageSizes: [7, 14, 28, 42],
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
type AccountNode = NonNullable<
|
|
@@ -77,22 +77,15 @@ const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
|
|
|
77
77
|
|
|
78
78
|
export default function AccountSearch() {
|
|
79
79
|
const [filtersOpen, setFiltersOpen] = useState(true);
|
|
80
|
-
const { data: industryOptions } =
|
|
81
|
-
|
|
82
|
-
ttl: 300_000,
|
|
83
|
-
});
|
|
84
|
-
const { data: typeOptions } = useCachedAsyncData(fetchDistinctTypes, [], {
|
|
85
|
-
key: "distinctTypes",
|
|
86
|
-
ttl: 300_000,
|
|
87
|
-
});
|
|
80
|
+
const { data: industryOptions } = useAsyncData(fetchDistinctIndustries, []);
|
|
81
|
+
const { data: typeOptions } = useAsyncData(fetchDistinctTypes, []);
|
|
88
82
|
|
|
89
83
|
const { filters, sort, query, pagination, resetAll } = useObjectSearchParams<
|
|
90
84
|
Account_Filter,
|
|
91
85
|
Account_OrderBy
|
|
92
86
|
>(FILTER_CONFIGS, ACCOUNT_SORT_CONFIGS, PAGINATION_CONFIG);
|
|
93
87
|
|
|
94
|
-
const
|
|
95
|
-
const { data, loading, error } = useCachedAsyncData(
|
|
88
|
+
const { data, loading, error } = useAsyncData(
|
|
96
89
|
() =>
|
|
97
90
|
searchAccounts({
|
|
98
91
|
where: query.where,
|
|
@@ -101,7 +94,6 @@ export default function AccountSearch() {
|
|
|
101
94
|
after: pagination.afterCursor,
|
|
102
95
|
}),
|
|
103
96
|
[query.where, query.orderBy, pagination.pageSize, pagination.afterCursor],
|
|
104
|
-
{ key: searchKey },
|
|
105
97
|
);
|
|
106
98
|
|
|
107
99
|
const pageInfo = data?.pageInfo;
|
|
@@ -196,7 +188,7 @@ export default function AccountSearch() {
|
|
|
196
188
|
<ActiveFilters filters={filters.active} onRemove={filters.remove} />
|
|
197
189
|
</div>
|
|
198
190
|
|
|
199
|
-
<div className="min-h-
|
|
191
|
+
<div className="min-h-132">
|
|
200
192
|
{/* Loading state */}
|
|
201
193
|
{loading && (
|
|
202
194
|
<>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
1
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
2
2
|
|
|
3
3
|
export interface ObjectSearchOptions<TWhere, TOrderBy> {
|
|
4
4
|
where?: TWhere;
|
|
@@ -21,24 +21,20 @@ export async function searchObjects<TResult, TQuery, TVariables>(
|
|
|
21
21
|
const { where, orderBy, first = 20, after } = options;
|
|
22
22
|
|
|
23
23
|
const data = await createDataSDK();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
after,
|
|
29
|
-
where,
|
|
30
|
-
orderBy,
|
|
31
|
-
} as TVariables,
|
|
24
|
+
const variables = { first, after, where, orderBy } as TVariables;
|
|
25
|
+
const result = await data.graphql!.query<TQuery, TVariables>({
|
|
26
|
+
query: query,
|
|
27
|
+
variables: variables,
|
|
32
28
|
});
|
|
33
29
|
|
|
34
|
-
if (
|
|
35
|
-
throw new Error(
|
|
30
|
+
if (result.errors?.length) {
|
|
31
|
+
throw new Error(result.errors.map((e) => e.message).join("; "));
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
const
|
|
34
|
+
const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
|
|
39
35
|
| Record<string, unknown>
|
|
40
36
|
| undefined;
|
|
41
|
-
const queryResult = (
|
|
37
|
+
const queryResult = (uiapi?.query as Record<string, unknown> | undefined)?.[objectName] as
|
|
42
38
|
| TResult
|
|
43
39
|
| undefined;
|
|
44
40
|
|
|
@@ -59,17 +55,16 @@ export async function fetchDistinctValues<TQuery>(
|
|
|
59
55
|
fieldName: string,
|
|
60
56
|
): Promise<PicklistOption[]> {
|
|
61
57
|
const data = await createDataSDK();
|
|
62
|
-
const
|
|
63
|
-
const errors = response?.errors;
|
|
58
|
+
const result = await data.graphql!.query<TQuery>({ query: query });
|
|
64
59
|
|
|
65
|
-
if (errors?.length) {
|
|
66
|
-
throw new Error(errors.map((e) => e.message).join("; "));
|
|
60
|
+
if (result.errors?.length) {
|
|
61
|
+
throw new Error(result.errors.map((e) => e.message).join("; "));
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
const
|
|
64
|
+
const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
|
|
70
65
|
| Record<string, unknown>
|
|
71
66
|
| undefined;
|
|
72
|
-
const aggregate = (
|
|
67
|
+
const aggregate = (uiapi?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
|
|
73
68
|
| { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
|
|
74
69
|
| undefined;
|
|
75
70
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import { Input } from "../../../../components/ui/input";
|
|
3
3
|
|
|
4
4
|
import { useFilterField } from "../FilterContext";
|
|
@@ -67,12 +67,16 @@ export function NumericRangeFilterInputs({
|
|
|
67
67
|
|
|
68
68
|
const externalMin = value?.min ?? "";
|
|
69
69
|
const externalMax = value?.max ?? "";
|
|
70
|
-
|
|
70
|
+
const [prevExternalMin, setPrevExternalMin] = useState(externalMin);
|
|
71
|
+
const [prevExternalMax, setPrevExternalMax] = useState(externalMax);
|
|
72
|
+
if (prevExternalMin !== externalMin) {
|
|
73
|
+
setPrevExternalMin(externalMin);
|
|
71
74
|
setLocalMin(externalMin);
|
|
72
|
-
}
|
|
73
|
-
|
|
75
|
+
}
|
|
76
|
+
if (prevExternalMax !== externalMax) {
|
|
77
|
+
setPrevExternalMax(externalMax);
|
|
74
78
|
setLocalMax(externalMax);
|
|
75
|
-
}
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
const isOutOfBounds = (v: string) => {
|
|
78
82
|
if (v === "") return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { SearchBar } from "../SearchBar";
|
|
4
4
|
import { useFilterField } from "../FilterContext";
|
|
@@ -22,9 +22,11 @@ export function SearchFilter({
|
|
|
22
22
|
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
23
23
|
|
|
24
24
|
const externalValue = value?.value ?? "";
|
|
25
|
-
|
|
25
|
+
const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
|
|
26
|
+
if (prevExternalValue !== externalValue) {
|
|
27
|
+
setPrevExternalValue(externalValue);
|
|
26
28
|
setLocalValue(externalValue);
|
|
27
|
-
}
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
30
32
|
if (v) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import { Input } from "../../../../components/ui/input";
|
|
3
3
|
import { cn } from "../../../../lib/utils";
|
|
4
4
|
import { useFilterField } from "../FilterContext";
|
|
@@ -62,9 +62,11 @@ export function TextFilterInput({
|
|
|
62
62
|
const [localValue, setLocalValue] = useState(value?.value ?? "");
|
|
63
63
|
|
|
64
64
|
const externalValue = value?.value ?? "";
|
|
65
|
-
|
|
65
|
+
const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
|
|
66
|
+
if (prevExternalValue !== externalValue) {
|
|
67
|
+
setPrevExternalValue(externalValue);
|
|
66
68
|
setLocalValue(externalValue);
|
|
67
|
-
}
|
|
69
|
+
}
|
|
68
70
|
|
|
69
71
|
const debouncedOnChange = useDebouncedCallback((v: string) => {
|
|
70
72
|
if (v) {
|
package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts
CHANGED
|
@@ -22,16 +22,24 @@ export function useAsyncData<T>(
|
|
|
22
22
|
const [data, setData] = useState<T | null>(null);
|
|
23
23
|
const [loading, setLoading] = useState(true);
|
|
24
24
|
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [generation, setGeneration] = useState(0);
|
|
25
26
|
|
|
26
27
|
const fetcherRef = useRef(fetcher);
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
fetcherRef.current = fetcher;
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
// Detect dep changes during render to reset loading state and bump generation
|
|
33
|
+
const [prevDeps, setPrevDeps] = useState(deps);
|
|
34
|
+
if (deps.length !== prevDeps.length || deps.some((d, i) => d !== prevDeps[i])) {
|
|
35
|
+
setPrevDeps(deps);
|
|
36
|
+
setGeneration((g) => g + 1);
|
|
37
|
+
if (!loading) setLoading(true);
|
|
38
|
+
if (error !== null) setError(null);
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
useEffect(() => {
|
|
32
42
|
let cancelled = false;
|
|
33
|
-
setLoading(true);
|
|
34
|
-
setError(null);
|
|
35
43
|
|
|
36
44
|
fetcherRef
|
|
37
45
|
.current()
|
|
@@ -49,8 +57,7 @@ export function useAsyncData<T>(
|
|
|
49
57
|
return () => {
|
|
50
58
|
cancelled = true;
|
|
51
59
|
};
|
|
52
|
-
|
|
53
|
-
}, deps);
|
|
60
|
+
}, [generation]);
|
|
54
61
|
|
|
55
62
|
return { data, loading, error };
|
|
56
63
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extensible user profile fetching and updating via UI API GraphQL.
|
|
3
3
|
*/
|
|
4
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
4
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
5
5
|
import { flattenGraphQLRecord } from "../utils/helpers";
|
|
6
6
|
|
|
7
7
|
const USER_PROFILE_FIELDS_FULL = `
|
|
@@ -46,9 +46,10 @@ function getUserProfileMutation(fields: string): string {
|
|
|
46
46
|
}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function throwOnGraphQLErrors(
|
|
50
|
-
if (
|
|
51
|
-
|
|
49
|
+
function throwOnGraphQLErrors(errors: { message: string }[] | undefined): void {
|
|
50
|
+
if (errors?.length) {
|
|
51
|
+
console.error("GraphQL request failed", errors);
|
|
52
|
+
throw new Error("An unexpected error occurred");
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -62,14 +63,14 @@ export async function fetchUserProfile<T>(
|
|
|
62
63
|
fields: string = USER_PROFILE_FIELDS_FULL,
|
|
63
64
|
): Promise<T> {
|
|
64
65
|
const data = await createDataSDK();
|
|
65
|
-
const
|
|
66
|
+
const result = await data.graphql!.query<any>({
|
|
66
67
|
query: getUserProfileQuery(fields),
|
|
67
68
|
variables: {
|
|
68
69
|
userId,
|
|
69
70
|
},
|
|
70
71
|
});
|
|
71
|
-
throwOnGraphQLErrors(
|
|
72
|
-
return flattenGraphQLRecord<T>(
|
|
72
|
+
throwOnGraphQLErrors(result.errors);
|
|
73
|
+
return flattenGraphQLRecord<T>(result.data?.uiapi?.query?.User?.edges?.[0]?.node);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -90,12 +91,12 @@ export async function updateUserProfile<T>(
|
|
|
90
91
|
values: Record<string, unknown>,
|
|
91
92
|
): Promise<T> {
|
|
92
93
|
const data = await createDataSDK();
|
|
93
|
-
const
|
|
94
|
-
|
|
94
|
+
const result = await data.graphql!.mutate<any>({
|
|
95
|
+
mutation: getUserProfileMutation(USER_PROFILE_FIELDS_FULL),
|
|
95
96
|
variables: {
|
|
96
97
|
input: { Id: userId, User: { ...values } },
|
|
97
98
|
},
|
|
98
99
|
});
|
|
99
|
-
throwOnGraphQLErrors(
|
|
100
|
-
return flattenGraphQLRecord<T>(
|
|
100
|
+
throwOnGraphQLErrors(result.errors);
|
|
101
|
+
return flattenGraphQLRecord<T>(result.data?.uiapi?.UserUpdate?.Record);
|
|
101
102
|
}
|
|
@@ -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>
|
package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/form.tsx
CHANGED
|
@@ -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();
|
|
@@ -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 { 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 && (
|
|
@@ -3,9 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
4
4
|
import { AuthForm } from "../forms/auth-form";
|
|
5
5
|
import { useAppForm } from "../hooks/form";
|
|
6
|
-
import { createDataSDK } from "@salesforce/sdk
|
|
6
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
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: () => {},
|