@salesforce/templates 66.8.0 → 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 (85) hide show
  1. package/lib/templates/project/reactexternalapp/AGENT.md +3 -3
  2. package/lib/templates/project/reactexternalapp/CHANGELOG.md +428 -0
  3. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +194 -1488
  4. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
  5. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  6. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  7. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  8. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  9. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  10. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  11. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  12. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/api/userProfileApi.ts +12 -11
  13. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/authenticationConfig.ts +9 -9
  14. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/context/AuthContext.tsx +21 -4
  15. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/forms/auth-form.tsx +15 -1
  16. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/form.tsx +1 -1
  17. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/privateRouteLayout.tsx +2 -11
  18. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ChangePassword.tsx +21 -5
  19. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ForgotPassword.tsx +20 -5
  20. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +20 -5
  21. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Profile.tsx +80 -43
  22. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +16 -5
  23. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ResetPassword.tsx +20 -5
  24. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/utils/helpers.ts +15 -52
  25. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  26. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  27. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  28. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  29. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  30. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  31. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +19 -25
  32. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  33. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  34. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleAuthUtils.cls-meta.xml +1 -1
  35. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleChangePassword.cls-meta.xml +1 -1
  36. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleForgotPassword.cls-meta.xml +1 -1
  37. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleLogin.cls-meta.xml +1 -1
  38. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleRegistration.cls-meta.xml +1 -1
  39. package/lib/templates/project/reactexternalapp/_p_/_m_/package.xml +1 -1
  40. package/lib/templates/project/reactexternalapp/package.json +1 -1
  41. package/lib/templates/project/reactexternalapp/scripts/org-setup.config.json +0 -1
  42. package/lib/templates/project/reactexternalapp/scripts/org-setup.mjs +528 -44
  43. package/lib/templates/project/reactexternalapp/sfdx-project.json +1 -1
  44. package/lib/templates/project/reactinternalapp/AGENT.md +3 -3
  45. package/lib/templates/project/reactinternalapp/CHANGELOG.md +428 -0
  46. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +214 -1521
  47. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +5 -5
  48. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.uibundle-meta.xml +1 -0
  49. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  50. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  51. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  52. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  53. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  54. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  55. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  56. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  57. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +40 -44
  58. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  59. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  60. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  61. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  62. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  63. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +9 -0
  64. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/globals.d.ts +13 -0
  65. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  66. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  67. package/lib/templates/project/reactinternalapp/_p_/_m_/applications/reactinternalapp.app-meta.xml +17 -0
  68. package/lib/templates/project/reactinternalapp/_p_/_m_/permissionsets/reactinternalapp_Access.permissionset-meta.xml +9 -0
  69. package/lib/templates/project/reactinternalapp/package.json +1 -1
  70. package/lib/templates/project/reactinternalapp/scripts/org-setup.config.json +6 -3
  71. package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +528 -44
  72. package/lib/templates/project/reactinternalapp/sfdx-project.json +1 -1
  73. package/lib/templates/uiBundles/reactbasic/package-lock.json +435 -43
  74. package/lib/templates/uiBundles/reactbasic/package.json +3 -3
  75. package/lib/templates/uiBundles/reactbasic/src/api/graphqlClient.ts +13 -13
  76. package/lib/templates/uiBundles/reactbasic/src/components/alerts/status-alert.tsx +11 -8
  77. package/lib/templates/uiBundles/reactbasic/src/components/ui/input.tsx +1 -1
  78. package/lib/templates/uiBundles/reactbasic/src/hooks/useAsyncData.ts +67 -0
  79. package/lib/templates/uiBundles/reactbasic/tsconfig.json +4 -6
  80. package/lib/templates/uiBundles/reactbasic/vite-env.d.ts +0 -3
  81. package/package.json +5 -5
  82. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
  83. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layout/card-skeleton.tsx +0 -38
  84. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/authenticationRouteLayout.tsx +0 -21
  85. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
@@ -1,28 +1,28 @@
1
1
  /**
2
- * Thin GraphQL client: createDataSDK + data.graphql with centralized error handling.
3
- * Use with gql-tagged queries and generated operation types for type-safe calls.
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-data';
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
- // SDK types graphql() first param as string; at runtime it may accept gql DocumentNode too
13
- const response = await data.graphql?.<TData, TVariables>({
14
- query,
15
- variables,
13
+ const result = await data.graphql!.query<TData, TVariables>({
14
+ query: query,
15
+ variables: variables,
16
16
  });
17
17
 
18
- if (!response) {
19
- throw new Error('GraphQL response is undefined');
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 (response?.errors?.length) {
23
- const msg = response.errors.map(e => e.message).join('; ');
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 response.data;
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 success messages.
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
- {isError ? (
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
+ }
@@ -28,11 +28,8 @@
28
28
  "@utils/*": ["./src/utils/*"],
29
29
  "@styles/*": ["./src/styles/*"],
30
30
  "@assets/*": ["./src/assets/*"],
31
- "@salesforce/sdk-core": [
32
- "../../../../../../../../../../packages/sdk/sdk-core/src/index.ts"
33
- ],
34
- "@salesforce/sdk-data": [
35
- "../../../../../../../../../../packages/sdk/sdk-data/src/index.ts"
31
+ "@salesforce/platform-sdk": [
32
+ "../../../../../../../../../../packages/sdk/platform-sdk/src/index.ts"
36
33
  ]
37
34
  }
38
35
  },
@@ -41,7 +38,8 @@
41
38
  "e2e",
42
39
  "vite-env.d.ts",
43
40
  "vitest-env.d.ts",
44
- "vitest.setup.ts"
41
+ "vitest.setup.ts",
42
+ "../../../../../../../../../../packages/sdk/platform-sdk/types"
45
43
  ],
46
44
  "references": [{ "path": "./tsconfig.node.json" }]
47
45
  }
@@ -2,6 +2,3 @@
2
2
 
3
3
  /** Salesforce API version injected at build time by the Vite define plugin. */
4
4
  declare const __SF_API_VERSION__: string;
5
-
6
- /** Package version of @salesforce/sdk-data injected at build time by its own Vite build. */
7
- declare const __SDK_DATA_VERSION__: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/templates",
3
- "version": "66.8.0",
3
+ "version": "66.9.0",
4
4
  "description": "Salesforce JS library for templates",
5
5
  "bugs": "https://github.com/forcedotcom/salesforcedx-templates/issues",
6
6
  "main": "lib/index.js",
@@ -25,10 +25,10 @@
25
25
  "@salesforce/dev-config": "^4.3.3",
26
26
  "@salesforce/dev-scripts": "^11.0.2",
27
27
  "@salesforce/prettier-config": "^0.0.4",
28
- "@salesforce/ui-bundle-template-app-react-template-b2e": "^1.135.0",
29
- "@salesforce/ui-bundle-template-app-react-template-b2x": "^1.135.0",
30
- "@salesforce/ui-bundle-template-base-react-app": "^1.135.0",
31
- "@salesforce/ui-bundle-template-base-web-app": "^1.135.0",
28
+ "@salesforce/ui-bundle-template-app-react-template-b2e": "^9.16.0",
29
+ "@salesforce/ui-bundle-template-app-react-template-b2x": "^9.16.0",
30
+ "@salesforce/ui-bundle-template-base-react-app": "^9.16.0",
31
+ "@salesforce/ui-bundle-template-base-web-app": "^9.16.0",
32
32
  "@types/chai-as-promised": "^7.1.8",
33
33
  "@types/ejs": "^3.1.5",
34
34
  "@types/mime-types": "^3.0.1",
@@ -1,188 +0,0 @@
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
- interface CacheOptions {
10
- /** Unique cache key. Used for lookups and invalidation via `clearCacheEntry`. */
11
- key: string;
12
- /** Time-to-live in ms. Default: 30_000 (30s) */
13
- ttl?: number;
14
- /** Max entries in the cache. Default: 50. Evicts oldest entry when exceeded. */
15
- maxSize?: number;
16
- }
17
-
18
- interface CacheEntry {
19
- data: unknown;
20
- timestamp: number;
21
- }
22
-
23
- /**
24
- * Module-level cache shared across all useCachedAsyncData consumers.
25
- * Cleared automatically on page reload since it only lives in memory.
26
- * Keys are caller-provided so different hook call-sites get independent entries.
27
- */
28
- const cache = new Map<string, CacheEntry>();
29
-
30
- /**
31
- * Returns a cached entry if it exists and hasn't exceeded its TTL.
32
- * Expired entries are deleted lazily here rather than on a timer,
33
- * so there's no background cleanup overhead.
34
- */
35
- function getValidEntry(key: string, ttl: number): CacheEntry | undefined {
36
- const entry = cache.get(key);
37
- if (!entry) return undefined;
38
- if (Date.now() - entry.timestamp > ttl) {
39
- cache.delete(key);
40
- return undefined;
41
- }
42
- return entry;
43
- }
44
-
45
- /**
46
- * Stores a result in the cache. If the cache is at capacity and the key
47
- * is new, the oldest entry (first in Map insertion order — FIFO) is evicted.
48
- */
49
- function setEntry(key: string, data: unknown, maxSize: number): void {
50
- if (!cache.has(key) && cache.size >= maxSize) {
51
- const firstKey = cache.keys().next().value;
52
- if (firstKey !== undefined) cache.delete(firstKey);
53
- }
54
- cache.set(key, { data, timestamp: Date.now() });
55
- }
56
-
57
- /**
58
- * Removes a single cache entry by key. The key must match the
59
- * `options.key` passed to `useCachedAsyncData`.
60
- *
61
- * @example
62
- * // Hook:
63
- * useCachedAsyncData(() => fetchAccountDetail(id), [id], { key: `account:${id}` });
64
- *
65
- * // Invalidate after a mutation:
66
- * await updateAccount(id, fields);
67
- * clearCacheEntry(`account:${id}`);
68
- */
69
- export function clearCacheEntry(key: string): void {
70
- cache.delete(key);
71
- }
72
-
73
- /**
74
- * Removes all cache entries. Useful for global invalidation scenarios
75
- * like user logout or after a bulk operation.
76
- *
77
- * @example
78
- * async function handleLogout() {
79
- * await logout();
80
- * clearCache();
81
- * navigate("/login");
82
- * }
83
- */
84
- export function clearCache(): void {
85
- cache.clear();
86
- }
87
-
88
- /**
89
- * Async data hook with in-memory caching. Works like `useAsyncData` but
90
- * avoids redundant network calls when the same data is requested again
91
- * (e.g. navigating away from a page and back).
92
- *
93
- * Cache behaviour:
94
- * - **Key**: Provided via `options.key`. Must be unique per logical data source.
95
- * Use the same key with `clearCacheEntry` to invalidate.
96
- * - **Hit**: Data is returned synchronously on the initial render —
97
- * `loading` starts as `false`, so there's no flash of a loading state.
98
- * - **Miss**: The fetcher runs, and the result is stored for future hits.
99
- * - **TTL**: Entries expire after `options.ttl` ms (default 30 s).
100
- * Expiry is checked lazily on read, not on a timer.
101
- * - **Max size**: Oldest entries are evicted FIFO when the cache exceeds
102
- * `options.maxSize` (default 50).
103
- *
104
- * @example
105
- * // Cache picklist options for 5 minutes (data rarely changes)
106
- * const { data: types } = useCachedAsyncData(fetchDistinctTypes, [], {
107
- * key: "distinctTypes",
108
- * ttl: 300_000,
109
- * });
110
- *
111
- * // Cache search results with default 30 s TTL (back-nav returns instantly)
112
- * const { data } = useCachedAsyncData(
113
- * () => searchAccounts({ where, orderBy, first, after }),
114
- * [where, orderBy, first, after],
115
- * { key: `accounts:${JSON.stringify({ where, orderBy, first, after })}` },
116
- * );
117
- *
118
- * // Invalidate a specific entry after a mutation
119
- * await updateAccount(id, fields);
120
- * clearCacheEntry(`account:${id}`);
121
- *
122
- * // Or clear everything (e.g. on logout)
123
- * clearCache();
124
- */
125
- export function useCachedAsyncData<T>(
126
- fetcher: () => Promise<T>,
127
- deps: React.DependencyList,
128
- options: CacheOptions,
129
- ): UseAsyncDataResult<T> {
130
- const ttl = options.ttl ?? 30_000;
131
- const maxSize = options.maxSize ?? 50;
132
- const cacheKey = options.key;
133
-
134
- // Synchronous cache check during state initialization so a cache hit
135
- // never triggers a loading → loaded transition (avoids UI flicker).
136
- const cached = getValidEntry(cacheKey, ttl);
137
-
138
- const [data, setData] = useState<T | null>((cached?.data as T) ?? null);
139
- const [loading, setLoading] = useState(!cached);
140
- const [error, setError] = useState<string | null>(null);
141
-
142
- const fetcherRef = useRef(fetcher);
143
- useEffect(() => {
144
- fetcherRef.current = fetcher;
145
- });
146
-
147
- useEffect(() => {
148
- // Re-check the cache inside the effect because deps may have changed
149
- // since the initial render (e.g. StrictMode double-invoke).
150
- const entry = getValidEntry(cacheKey, ttl);
151
- if (entry) {
152
- setData(entry.data as T);
153
- setLoading(false);
154
- setError(null);
155
- return;
156
- }
157
-
158
- // No cache hit — fetch from the network.
159
- let cancelled = false;
160
- setLoading(true);
161
- setError(null);
162
-
163
- fetcherRef
164
- .current()
165
- .then((result) => {
166
- if (!cancelled) {
167
- setEntry(cacheKey, result, maxSize);
168
- setData(result);
169
- }
170
- })
171
- .catch((err) => {
172
- console.error(err);
173
- if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
174
- })
175
- .finally(() => {
176
- if (!cancelled) setLoading(false);
177
- });
178
-
179
- // Cleanup: if deps change or the component unmounts before the fetch
180
- // completes, the cancelled flag prevents stale state updates.
181
- return () => {
182
- cancelled = true;
183
- };
184
- // eslint-disable-next-line react-hooks/exhaustive-deps --- deps are explicitly managed by the caller
185
- }, [...deps, cacheKey, ttl, maxSize]);
186
-
187
- return { data, loading, error };
188
- }
@@ -1,38 +0,0 @@
1
- import { CenteredPageLayout } from "./centered-page-layout";
2
- import { Card, CardContent, CardHeader } from "../../../components/ui/card";
3
- import { Skeleton } from "../../../components/ui/skeleton";
4
-
5
- interface CardSkeletonProps {
6
- /**
7
- * Maximum width of the content container.
8
- * @default "sm"
9
- */
10
- contentMaxWidth?: "sm" | "md" | "lg";
11
- /**
12
- * Accessible label for screen readers.
13
- * @default "Loading…"
14
- */
15
- loadingText?: string;
16
- }
17
-
18
- /**
19
- * Full-page loading indicator with skeleton card placeholder.
20
- */
21
- export function CardSkeleton({ contentMaxWidth, loadingText = "Loading…" }: CardSkeletonProps) {
22
- return (
23
- <CenteredPageLayout contentMaxWidth={contentMaxWidth}>
24
- <div role="status" aria-live="polite">
25
- <Card className="w-full">
26
- <CardHeader>
27
- <Skeleton className="h-4 w-2/3" />
28
- <Skeleton className="h-4 w-1/2" />
29
- </CardHeader>
30
- <CardContent>
31
- <Skeleton className="aspect-video w-full" />
32
- </CardContent>
33
- </Card>
34
- <span className="sr-only">{loadingText}</span>
35
- </div>
36
- </CenteredPageLayout>
37
- );
38
- }
@@ -1,21 +0,0 @@
1
- import { Navigate, Outlet, useSearchParams } from "react-router";
2
- import { useAuth } from "../context/AuthContext";
3
- import { getStartUrl } from "../authHelpers";
4
- import { CardSkeleton } from "../layout/card-skeleton";
5
-
6
- /**
7
- * [Dev Note] "Public Only" Route Guard:
8
- * This component protects routes that should NOT be accessible if the user is already logged in
9
- * (e.g., Login, Register, Forgot Password).
10
- * If an authenticated user tries to access these pages, they are automatically redirected
11
- * to the default authenticated view (e.g., Home or Profile) to prevent confusion.
12
- */
13
- export default function AuthenticationRoute() {
14
- const { isAuthenticated, loading } = useAuth();
15
- const [searchParams] = useSearchParams();
16
-
17
- if (loading) return <CardSkeleton contentMaxWidth="md" />;
18
- if (isAuthenticated) return <Navigate to={getStartUrl(searchParams)} replace />;
19
-
20
- return <Outlet />;
21
- }
@@ -1,188 +0,0 @@
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
- interface CacheOptions {
10
- /** Unique cache key. Used for lookups and invalidation via `clearCacheEntry`. */
11
- key: string;
12
- /** Time-to-live in ms. Default: 30_000 (30s) */
13
- ttl?: number;
14
- /** Max entries in the cache. Default: 50. Evicts oldest entry when exceeded. */
15
- maxSize?: number;
16
- }
17
-
18
- interface CacheEntry {
19
- data: unknown;
20
- timestamp: number;
21
- }
22
-
23
- /**
24
- * Module-level cache shared across all useCachedAsyncData consumers.
25
- * Cleared automatically on page reload since it only lives in memory.
26
- * Keys are caller-provided so different hook call-sites get independent entries.
27
- */
28
- const cache = new Map<string, CacheEntry>();
29
-
30
- /**
31
- * Returns a cached entry if it exists and hasn't exceeded its TTL.
32
- * Expired entries are deleted lazily here rather than on a timer,
33
- * so there's no background cleanup overhead.
34
- */
35
- function getValidEntry(key: string, ttl: number): CacheEntry | undefined {
36
- const entry = cache.get(key);
37
- if (!entry) return undefined;
38
- if (Date.now() - entry.timestamp > ttl) {
39
- cache.delete(key);
40
- return undefined;
41
- }
42
- return entry;
43
- }
44
-
45
- /**
46
- * Stores a result in the cache. If the cache is at capacity and the key
47
- * is new, the oldest entry (first in Map insertion order — FIFO) is evicted.
48
- */
49
- function setEntry(key: string, data: unknown, maxSize: number): void {
50
- if (!cache.has(key) && cache.size >= maxSize) {
51
- const firstKey = cache.keys().next().value;
52
- if (firstKey !== undefined) cache.delete(firstKey);
53
- }
54
- cache.set(key, { data, timestamp: Date.now() });
55
- }
56
-
57
- /**
58
- * Removes a single cache entry by key. The key must match the
59
- * `options.key` passed to `useCachedAsyncData`.
60
- *
61
- * @example
62
- * // Hook:
63
- * useCachedAsyncData(() => fetchAccountDetail(id), [id], { key: `account:${id}` });
64
- *
65
- * // Invalidate after a mutation:
66
- * await updateAccount(id, fields);
67
- * clearCacheEntry(`account:${id}`);
68
- */
69
- export function clearCacheEntry(key: string): void {
70
- cache.delete(key);
71
- }
72
-
73
- /**
74
- * Removes all cache entries. Useful for global invalidation scenarios
75
- * like user logout or after a bulk operation.
76
- *
77
- * @example
78
- * async function handleLogout() {
79
- * await logout();
80
- * clearCache();
81
- * navigate("/login");
82
- * }
83
- */
84
- export function clearCache(): void {
85
- cache.clear();
86
- }
87
-
88
- /**
89
- * Async data hook with in-memory caching. Works like `useAsyncData` but
90
- * avoids redundant network calls when the same data is requested again
91
- * (e.g. navigating away from a page and back).
92
- *
93
- * Cache behaviour:
94
- * - **Key**: Provided via `options.key`. Must be unique per logical data source.
95
- * Use the same key with `clearCacheEntry` to invalidate.
96
- * - **Hit**: Data is returned synchronously on the initial render —
97
- * `loading` starts as `false`, so there's no flash of a loading state.
98
- * - **Miss**: The fetcher runs, and the result is stored for future hits.
99
- * - **TTL**: Entries expire after `options.ttl` ms (default 30 s).
100
- * Expiry is checked lazily on read, not on a timer.
101
- * - **Max size**: Oldest entries are evicted FIFO when the cache exceeds
102
- * `options.maxSize` (default 50).
103
- *
104
- * @example
105
- * // Cache picklist options for 5 minutes (data rarely changes)
106
- * const { data: types } = useCachedAsyncData(fetchDistinctTypes, [], {
107
- * key: "distinctTypes",
108
- * ttl: 300_000,
109
- * });
110
- *
111
- * // Cache search results with default 30 s TTL (back-nav returns instantly)
112
- * const { data } = useCachedAsyncData(
113
- * () => searchAccounts({ where, orderBy, first, after }),
114
- * [where, orderBy, first, after],
115
- * { key: `accounts:${JSON.stringify({ where, orderBy, first, after })}` },
116
- * );
117
- *
118
- * // Invalidate a specific entry after a mutation
119
- * await updateAccount(id, fields);
120
- * clearCacheEntry(`account:${id}`);
121
- *
122
- * // Or clear everything (e.g. on logout)
123
- * clearCache();
124
- */
125
- export function useCachedAsyncData<T>(
126
- fetcher: () => Promise<T>,
127
- deps: React.DependencyList,
128
- options: CacheOptions,
129
- ): UseAsyncDataResult<T> {
130
- const ttl = options.ttl ?? 30_000;
131
- const maxSize = options.maxSize ?? 50;
132
- const cacheKey = options.key;
133
-
134
- // Synchronous cache check during state initialization so a cache hit
135
- // never triggers a loading → loaded transition (avoids UI flicker).
136
- const cached = getValidEntry(cacheKey, ttl);
137
-
138
- const [data, setData] = useState<T | null>((cached?.data as T) ?? null);
139
- const [loading, setLoading] = useState(!cached);
140
- const [error, setError] = useState<string | null>(null);
141
-
142
- const fetcherRef = useRef(fetcher);
143
- useEffect(() => {
144
- fetcherRef.current = fetcher;
145
- });
146
-
147
- useEffect(() => {
148
- // Re-check the cache inside the effect because deps may have changed
149
- // since the initial render (e.g. StrictMode double-invoke).
150
- const entry = getValidEntry(cacheKey, ttl);
151
- if (entry) {
152
- setData(entry.data as T);
153
- setLoading(false);
154
- setError(null);
155
- return;
156
- }
157
-
158
- // No cache hit — fetch from the network.
159
- let cancelled = false;
160
- setLoading(true);
161
- setError(null);
162
-
163
- fetcherRef
164
- .current()
165
- .then((result) => {
166
- if (!cancelled) {
167
- setEntry(cacheKey, result, maxSize);
168
- setData(result);
169
- }
170
- })
171
- .catch((err) => {
172
- console.error(err);
173
- if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
174
- })
175
- .finally(() => {
176
- if (!cancelled) setLoading(false);
177
- });
178
-
179
- // Cleanup: if deps change or the component unmounts before the fetch
180
- // completes, the cancelled flag prevents stale state updates.
181
- return () => {
182
- cancelled = true;
183
- };
184
- // eslint-disable-next-line react-hooks/exhaustive-deps --- deps are explicitly managed by the caller
185
- }, [...deps, cacheKey, ttl, maxSize]);
186
-
187
- return { data, loading, error };
188
- }