@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.
Files changed (105) hide show
  1. package/lib/generators/lightningEmbeddingGenerator.d.ts +6 -0
  2. package/lib/generators/lightningEmbeddingGenerator.js +68 -0
  3. package/lib/generators/lightningEmbeddingGenerator.js.map +1 -0
  4. package/lib/i18n/i18n.d.ts +6 -0
  5. package/lib/i18n/i18n.js +6 -0
  6. package/lib/i18n/i18n.js.map +1 -1
  7. package/lib/index.d.ts +1 -0
  8. package/lib/index.js +1 -0
  9. package/lib/index.js.map +1 -1
  10. package/lib/templates/lightningembedding/default/default.css +5 -0
  11. package/lib/templates/lightningembedding/default/default.html +7 -0
  12. package/lib/templates/lightningembedding/default/default.js +5 -0
  13. package/lib/templates/lightningembedding/default/default.js-meta.xml +12 -0
  14. package/lib/templates/project/reactexternalapp/AGENT.md +3 -3
  15. package/lib/templates/project/reactexternalapp/CHANGELOG.md +428 -0
  16. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +792 -2031
  17. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
  18. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  19. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  20. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  21. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  22. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  23. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  24. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  25. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/api/userProfileApi.ts +12 -11
  26. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/authenticationConfig.ts +9 -9
  27. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/context/AuthContext.tsx +21 -4
  28. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/forms/auth-form.tsx +15 -1
  29. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/form.tsx +1 -1
  30. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/privateRouteLayout.tsx +2 -11
  31. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ChangePassword.tsx +21 -5
  32. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ForgotPassword.tsx +20 -5
  33. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +20 -5
  34. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Profile.tsx +80 -43
  35. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +16 -5
  36. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ResetPassword.tsx +20 -5
  37. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/utils/helpers.ts +15 -52
  38. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  39. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  40. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  41. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  42. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  43. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  44. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +19 -25
  45. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  46. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  47. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleAuthUtils.cls-meta.xml +1 -1
  48. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleChangePassword.cls-meta.xml +1 -1
  49. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleForgotPassword.cls-meta.xml +1 -1
  50. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleLogin.cls-meta.xml +1 -1
  51. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleRegistration.cls-meta.xml +1 -1
  52. package/lib/templates/project/reactexternalapp/_p_/_m_/package.xml +1 -1
  53. package/lib/templates/project/reactexternalapp/package.json +1 -1
  54. package/lib/templates/project/reactexternalapp/scripts/org-setup.config.json +0 -1
  55. package/lib/templates/project/reactexternalapp/scripts/org-setup.mjs +528 -44
  56. package/lib/templates/project/reactexternalapp/sfdx-project.json +1 -1
  57. package/lib/templates/project/reactinternalapp/AGENT.md +3 -3
  58. package/lib/templates/project/reactinternalapp/CHANGELOG.md +428 -0
  59. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +784 -2036
  60. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +5 -5
  61. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.uibundle-meta.xml +1 -0
  62. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  63. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  64. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  65. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  66. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  67. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  68. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  69. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  70. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +40 -44
  71. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  72. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  73. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  74. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  75. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  76. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +9 -0
  77. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/globals.d.ts +13 -0
  78. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  79. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  80. package/lib/templates/project/reactinternalapp/_p_/_m_/applications/reactinternalapp.app-meta.xml +17 -0
  81. package/lib/templates/project/reactinternalapp/_p_/_m_/permissionsets/reactinternalapp_Access.permissionset-meta.xml +9 -0
  82. package/lib/templates/project/reactinternalapp/package.json +1 -1
  83. package/lib/templates/project/reactinternalapp/scripts/org-setup.config.json +6 -3
  84. package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +528 -44
  85. package/lib/templates/project/reactinternalapp/sfdx-project.json +1 -1
  86. package/lib/templates/uiBundles/reactbasic/package-lock.json +1040 -593
  87. package/lib/templates/uiBundles/reactbasic/package.json +3 -3
  88. package/lib/templates/uiBundles/reactbasic/src/api/graphqlClient.ts +13 -13
  89. package/lib/templates/uiBundles/reactbasic/src/components/alerts/status-alert.tsx +11 -8
  90. package/lib/templates/uiBundles/reactbasic/src/components/ui/input.tsx +1 -1
  91. package/lib/templates/uiBundles/reactbasic/src/hooks/useAsyncData.ts +67 -0
  92. package/lib/templates/uiBundles/reactbasic/tsconfig.json +4 -6
  93. package/lib/templates/uiBundles/reactbasic/vite-env.d.ts +0 -3
  94. package/lib/tsconfig.tsbuildinfo +1 -1
  95. package/lib/utils/lightningEmbedding.d.ts +12 -0
  96. package/lib/utils/lightningEmbedding.js +50 -0
  97. package/lib/utils/lightningEmbedding.js.map +1 -0
  98. package/lib/utils/types.d.ts +15 -6
  99. package/lib/utils/types.js +8 -5
  100. package/lib/utils/types.js.map +1 -1
  101. package/package.json +6 -6
  102. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
  103. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layout/card-skeleton.tsx +0 -38
  104. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/authenticationRouteLayout.tsx +0 -21
  105. 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-data": "^1.135.0",
22
- "@salesforce/ui-bundle": "^1.135.0",
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": "^1.135.0",
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-data";
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 { useCachedAsyncData } from "../../hooks/useCachedAsyncData";
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 response = await data.graphql?.<GetAccountDetailQuery, GetAccountDetailQueryVariables>({
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 (response?.errors?.length) {
38
- throw new Error(response.errors.map((e) => e.message).join("; "));
37
+ if (result.errors?.length) {
38
+ throw new Error(result.errors.map((e) => e.message).join("; "));
39
39
  }
40
40
 
41
- return response?.data?.uiapi?.query?.Account?.edges?.[0]?.node;
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
- } = useCachedAsyncData(() => fetchAccountDetail(recordId!), [recordId], {
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 { useCachedAsyncData } from "../../hooks/useCachedAsyncData";
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: 6,
46
- validPageSizes: [6, 12, 24, 48],
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 } = useCachedAsyncData(fetchDistinctIndustries, [], {
81
- key: "distinctIndustries",
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 searchKey = `accounts:${JSON.stringify({ where: query.where, orderBy: query.orderBy, first: pagination.pageSize, after: pagination.afterCursor })}`;
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-112">
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-data";
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 response = await data.graphql?.<TQuery, TVariables>({
25
- query,
26
- variables: {
27
- first,
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 (response?.errors?.length) {
35
- throw new Error(response.errors.map((e) => e.message).join("; "));
30
+ if (result.errors?.length) {
31
+ throw new Error(result.errors.map((e) => e.message).join("; "));
36
32
  }
37
33
 
38
- const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
34
+ const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
39
35
  | Record<string, unknown>
40
36
  | undefined;
41
- const queryResult = (result?.query as Record<string, unknown> | undefined)?.[objectName] as
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 response = await data.graphql?.<TQuery>({ query });
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 result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
64
+ const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
70
65
  | Record<string, unknown>
71
66
  | undefined;
72
- const aggregate = (result?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
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 { useEffect, useState } from "react";
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
- useEffect(() => {
70
+ const [prevExternalMin, setPrevExternalMin] = useState(externalMin);
71
+ const [prevExternalMax, setPrevExternalMax] = useState(externalMax);
72
+ if (prevExternalMin !== externalMin) {
73
+ setPrevExternalMin(externalMin);
71
74
  setLocalMin(externalMin);
72
- }, [externalMin]);
73
- useEffect(() => {
75
+ }
76
+ if (prevExternalMax !== externalMax) {
77
+ setPrevExternalMax(externalMax);
74
78
  setLocalMax(externalMax);
75
- }, [externalMax]);
79
+ }
76
80
 
77
81
  const isOutOfBounds = (v: string) => {
78
82
  if (v === "") return false;
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
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
- useEffect(() => {
25
+ const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
26
+ if (prevExternalValue !== externalValue) {
27
+ setPrevExternalValue(externalValue);
26
28
  setLocalValue(externalValue);
27
- }, [externalValue]);
29
+ }
28
30
 
29
31
  const debouncedOnChange = useDebouncedCallback((v: string) => {
30
32
  if (v) {
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
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
- useEffect(() => {
65
+ const [prevExternalValue, setPrevExternalValue] = useState(externalValue);
66
+ if (prevExternalValue !== externalValue) {
67
+ setPrevExternalValue(externalValue);
66
68
  setLocalValue(externalValue);
67
- }, [externalValue]);
69
+ }
68
70
 
69
71
  const debouncedOnChange = useDebouncedCallback((v: string) => {
70
72
  if (v) {
@@ -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
- // eslint-disable-next-line react-hooks/exhaustive-deps --- deps are explicitly managed by the caller
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-data";
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(response: any): void {
50
- if (response?.errors?.length) {
51
- throw new Error(response.errors.map((e: any) => e.message).join("; "));
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 response: any = await data.graphql?.({
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(response);
72
- return flattenGraphQLRecord<T>(response?.data?.uiapi?.query?.User?.edges?.[0]?.node);
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 response: any = await data.graphql?.({
94
- query: getUserProfileMutation(USER_PROFILE_FIELDS_FULL),
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(response);
100
- return flattenGraphQLRecord<T>(response?.data?.uiapi?.UserUpdate?.Record);
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: "asalesforce@example.com",
53
- PASSWORD: "",
54
- PASSWORD_CREATE: "",
55
- PASSWORD_CONFIRM: "",
56
- PASSWORD_NEW: "",
57
- PASSWORD_NEW_CONFIRM: "",
58
- FIRST_NAME: "Astro",
59
- LAST_NAME: "Salesforce",
60
- USERNAME: "asalesforce",
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
- const errorMessage = err instanceof Error ? err.message : "Authentication failed";
39
- setError(errorMessage);
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
- checkAuth();
57
- }, [checkAuth]);
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={submit.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.isTouched && field.state.meta.errors.length > 0;
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({ showCardSkeleton = false }: PrivateRouteProps) {
11
+ export default function PrivateRoute() {
21
12
  const { isAuthenticated, loading } = useAuth();
22
13
  const location = useLocation();
23
14
 
24
- if (loading) return showCardSkeleton ? <CardSkeleton contentMaxWidth="md" /> : null;
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-data";
7
+ import { createDataSDK } from "@salesforce/platform-sdk";
8
8
  import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
9
9
  import { newPasswordSchema } from "../authHelpers";
10
- import { handleApiResponse, getErrorMessage } from "../utils/helpers";
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<string | null>(null);
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, "Password change failed");
43
+ await handleApiResponse(response);
44
44
  setSuccess(true);
45
45
  form.reset();
46
46
  } catch (err) {
47
- setSubmitError(getErrorMessage(err, "Password change failed"));
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-data";
6
+ import { createDataSDK } from "@salesforce/platform-sdk";
7
7
  import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
8
- import { handleApiResponse, getErrorMessage } from "../utils/helpers";
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<string | null>(null);
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, "Failed to send reset link");
36
+ await handleApiResponse(response);
37
37
  setSuccess(true);
38
38
  } catch (err) {
39
- setSubmitError(getErrorMessage(err, "Failed to send reset link"));
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: () => {},