@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,8 +18,8 @@
|
|
|
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": "^9.20.0",
|
|
22
|
+
"@salesforce/ui-bundle": "^9.20.0",
|
|
23
23
|
"@tailwindcss/vite": "^4.1.17",
|
|
24
24
|
"class-variance-authority": "^0.7.1",
|
|
25
25
|
"clsx": "^2.1.1",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
45
45
|
"@graphql-tools/utils": "^11.0.0",
|
|
46
46
|
"@playwright/test": "^1.49.0",
|
|
47
|
-
"@salesforce/vite-plugin-ui-bundle": "^
|
|
47
|
+
"@salesforce/vite-plugin-ui-bundle": "^9.20.0",
|
|
48
48
|
"@testing-library/jest-dom": "^6.6.3",
|
|
49
49
|
"@testing-library/react": "^16.1.0",
|
|
50
50
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thin GraphQL client: createDataSDK +
|
|
3
|
-
* Use with gql-tagged queries and generated operation types
|
|
2
|
+
* Thin GraphQL client: createDataSDK + sdk.graphql.query with centralized
|
|
3
|
+
* error handling. Use with gql-tagged queries and generated operation types
|
|
4
|
+
* for type-safe calls.
|
|
4
5
|
*/
|
|
5
|
-
import { createDataSDK } from '@salesforce/sdk
|
|
6
|
+
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
6
7
|
|
|
7
8
|
export async function executeGraphQL<TData, TVariables>(
|
|
8
9
|
query: string,
|
|
9
10
|
variables?: TVariables
|
|
10
11
|
): Promise<TData> {
|
|
11
12
|
const data = await createDataSDK();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
variables,
|
|
13
|
+
const result = await data.graphql!.query<TData, TVariables>({
|
|
14
|
+
query: query,
|
|
15
|
+
variables: variables,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
18
|
+
if (result.errors?.length) {
|
|
19
|
+
const msg = result.errors.map(e => e.message).join('; ');
|
|
20
|
+
throw new Error(`GraphQL Error: ${msg}`);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
throw new Error(`GraphQL Error: ${msg}`);
|
|
23
|
+
if (result.data == null) {
|
|
24
|
+
throw new Error('GraphQL response data is null');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
return
|
|
27
|
+
return result.data;
|
|
28
28
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import { AlertCircleIcon, CheckCircle2Icon } from 'lucide-react';
|
|
2
|
+
import { AlertCircleIcon, CheckCircle2Icon, InfoIcon } from 'lucide-react';
|
|
3
3
|
import { Alert, AlertDescription } from '../../components/ui/alert';
|
|
4
4
|
import { useId } from 'react';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ const statusAlertVariants = cva('', {
|
|
|
8
8
|
variant: {
|
|
9
9
|
error: '',
|
|
10
10
|
success: '',
|
|
11
|
+
info: 'text-blue-600 *:[svg]:text-current *:data-[slot=alert-description]:text-blue-600/90',
|
|
11
12
|
},
|
|
12
13
|
},
|
|
13
14
|
defaultVariants: {
|
|
@@ -18,11 +19,11 @@ const statusAlertVariants = cva('', {
|
|
|
18
19
|
interface StatusAlertProps extends VariantProps<typeof statusAlertVariants> {
|
|
19
20
|
children?: React.ReactNode;
|
|
20
21
|
/** Alert variant type. @default "error" */
|
|
21
|
-
variant?: 'error' | 'success';
|
|
22
|
+
variant?: 'error' | 'success' | 'info';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Status alert component for displaying error or
|
|
26
|
+
* Status alert component for displaying error, success, or info messages.
|
|
26
27
|
* Returns null if no children are provided.
|
|
27
28
|
*/
|
|
28
29
|
export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
@@ -31,6 +32,12 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
31
32
|
|
|
32
33
|
const isError = variant === 'error';
|
|
33
34
|
|
|
35
|
+
const icon = {
|
|
36
|
+
error: <AlertCircleIcon aria-hidden="true" />,
|
|
37
|
+
success: <CheckCircle2Icon aria-hidden="true" />,
|
|
38
|
+
info: <InfoIcon aria-hidden="true" />,
|
|
39
|
+
}[variant];
|
|
40
|
+
|
|
34
41
|
return (
|
|
35
42
|
<Alert
|
|
36
43
|
variant={isError ? 'destructive' : 'default'}
|
|
@@ -38,11 +45,7 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
38
45
|
aria-describedby={descriptionId}
|
|
39
46
|
role={isError ? 'alert' : 'status'}
|
|
40
47
|
>
|
|
41
|
-
{
|
|
42
|
-
<AlertCircleIcon aria-hidden="true" />
|
|
43
|
-
) : (
|
|
44
|
-
<CheckCircle2Icon aria-hidden="true" />
|
|
45
|
-
)}
|
|
48
|
+
{icon}
|
|
46
49
|
<AlertDescription id={descriptionId}>{children}</AlertDescription>
|
|
47
50
|
</Alert>
|
|
48
51
|
);
|
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
|
8
8
|
type={type}
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
|
-
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
11
|
+
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground/70 placeholder:italic w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseAsyncDataResult<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs an async fetcher on mount and whenever `deps` change.
|
|
11
|
+
* Returns the loading/error/data state. Does not cache — every call
|
|
12
|
+
* to the fetcher hits the source directly.
|
|
13
|
+
*
|
|
14
|
+
* A cleanup flag prevents state updates if the component unmounts
|
|
15
|
+
* or deps change before the fetch completes (avoids React warnings
|
|
16
|
+
* and stale updates from out-of-order responses).
|
|
17
|
+
*/
|
|
18
|
+
export function useAsyncData<T>(
|
|
19
|
+
fetcher: () => Promise<T>,
|
|
20
|
+
deps: React.DependencyList
|
|
21
|
+
): UseAsyncDataResult<T> {
|
|
22
|
+
const [data, setData] = useState<T | null>(null);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [generation, setGeneration] = useState(0);
|
|
26
|
+
|
|
27
|
+
const fetcherRef = useRef(fetcher);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetcherRef.current = fetcher;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Detect dep changes during render to reset loading state and bump generation
|
|
33
|
+
const [prevDeps, setPrevDeps] = useState(deps);
|
|
34
|
+
if (
|
|
35
|
+
deps.length !== prevDeps.length ||
|
|
36
|
+
deps.some((d, i) => d !== prevDeps[i])
|
|
37
|
+
) {
|
|
38
|
+
setPrevDeps(deps);
|
|
39
|
+
setGeneration(g => g + 1);
|
|
40
|
+
if (!loading) setLoading(true);
|
|
41
|
+
if (error !== null) setError(null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
|
|
47
|
+
fetcherRef
|
|
48
|
+
.current()
|
|
49
|
+
.then(result => {
|
|
50
|
+
if (!cancelled) setData(result);
|
|
51
|
+
})
|
|
52
|
+
.catch(err => {
|
|
53
|
+
console.error(err);
|
|
54
|
+
if (!cancelled)
|
|
55
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (!cancelled) setLoading(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [generation]);
|
|
65
|
+
|
|
66
|
+
return { data, loading, error };
|
|
67
|
+
}
|
|
@@ -28,11 +28,8 @@
|
|
|
28
28
|
"@utils/*": ["./src/utils/*"],
|
|
29
29
|
"@styles/*": ["./src/styles/*"],
|
|
30
30
|
"@assets/*": ["./src/assets/*"],
|
|
31
|
-
"@salesforce/sdk
|
|
32
|
-
"../../../../../../../../../../packages/sdk/sdk
|
|
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
|
}
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/index.ts","../src/generators/analyticsTemplateGenerator.ts","../src/generators/apexClassGenerator.ts","../src/generators/apexTriggerGenerator.ts","../src/generators/baseGenerator.ts","../src/generators/digitalExperienceSiteGenerator.ts","../src/generators/flexipageGenerator.ts","../src/generators/lightningAppGenerator.ts","../src/generators/lightningComponentGenerator.ts","../src/generators/lightningEventGenerator.ts","../src/generators/lightningInterfaceGenerator.ts","../src/generators/lightningTestGenerator.ts","../src/generators/projectGenerator.ts","../src/generators/staticResourceGenerator.ts","../src/generators/uiBundleGenerator.ts","../src/generators/visualforceComponentGenerator.ts","../src/generators/visualforcePageGenerator.ts","../src/i18n/i18n.ts","../src/i18n/index.ts","../src/i18n/localization.ts","../src/service/gitRepoUtils.ts","../src/service/templateService.ts","../src/utils/constants.ts","../src/utils/createUtil.ts","../src/utils/index.ts","../src/utils/template-placeholders.ts","../src/utils/types.ts","../src/utils/uiBundleTemplateUtils.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/index.ts","../src/generators/analyticsTemplateGenerator.ts","../src/generators/apexClassGenerator.ts","../src/generators/apexTriggerGenerator.ts","../src/generators/baseGenerator.ts","../src/generators/digitalExperienceSiteGenerator.ts","../src/generators/flexipageGenerator.ts","../src/generators/lightningAppGenerator.ts","../src/generators/lightningComponentGenerator.ts","../src/generators/lightningEmbeddingGenerator.ts","../src/generators/lightningEventGenerator.ts","../src/generators/lightningInterfaceGenerator.ts","../src/generators/lightningTestGenerator.ts","../src/generators/projectGenerator.ts","../src/generators/staticResourceGenerator.ts","../src/generators/uiBundleGenerator.ts","../src/generators/visualforceComponentGenerator.ts","../src/generators/visualforcePageGenerator.ts","../src/i18n/i18n.ts","../src/i18n/index.ts","../src/i18n/localization.ts","../src/service/gitRepoUtils.ts","../src/service/templateService.ts","../src/utils/constants.ts","../src/utils/createUtil.ts","../src/utils/index.ts","../src/utils/lightningEmbedding.ts","../src/utils/template-placeholders.ts","../src/utils/types.ts","../src/utils/uiBundleTemplateUtils.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C-defined iframe sandbox tokens accepted by the lightning-embedding wrapper template.
|
|
3
|
+
* Exposed so CLI plugins can derive their flag `options` list from a single source of truth.
|
|
4
|
+
*/
|
|
5
|
+
export declare const LIGHTNING_EMBEDDING_SANDBOX_TOKENS: readonly ["allow-forms", "allow-modals", "allow-orientation-lock", "allow-pointer-lock", "allow-popups", "allow-popups-to-escape-sandbox", "allow-presentation", "allow-same-origin", "allow-scripts", "allow-storage-access-by-user-activation", "allow-top-navigation", "allow-top-navigation-by-user-activation"];
|
|
6
|
+
export type LightningEmbeddingSandboxToken = (typeof LIGHTNING_EMBEDDING_SANDBOX_TOKENS)[number];
|
|
7
|
+
/**
|
|
8
|
+
* Returns true if `src` is an absolute URL acceptable as the iframe source on the
|
|
9
|
+
* lightning-embedding wrapper. https is accepted everywhere; plain http is permitted
|
|
10
|
+
* only for localhost / 127.0.0.1 to support local development servers.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAllowedLightningEmbeddingSrcUrl(src: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
* Licensed under the BSD 3-Clause license.
|
|
6
|
+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.LIGHTNING_EMBEDDING_SANDBOX_TOKENS = void 0;
|
|
10
|
+
exports.isAllowedLightningEmbeddingSrcUrl = isAllowedLightningEmbeddingSrcUrl;
|
|
11
|
+
/**
|
|
12
|
+
* W3C-defined iframe sandbox tokens accepted by the lightning-embedding wrapper template.
|
|
13
|
+
* Exposed so CLI plugins can derive their flag `options` list from a single source of truth.
|
|
14
|
+
*/
|
|
15
|
+
exports.LIGHTNING_EMBEDDING_SANDBOX_TOKENS = [
|
|
16
|
+
'allow-forms',
|
|
17
|
+
'allow-modals',
|
|
18
|
+
'allow-orientation-lock',
|
|
19
|
+
'allow-pointer-lock',
|
|
20
|
+
'allow-popups',
|
|
21
|
+
'allow-popups-to-escape-sandbox',
|
|
22
|
+
'allow-presentation',
|
|
23
|
+
'allow-same-origin',
|
|
24
|
+
'allow-scripts',
|
|
25
|
+
'allow-storage-access-by-user-activation',
|
|
26
|
+
'allow-top-navigation',
|
|
27
|
+
'allow-top-navigation-by-user-activation',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if `src` is an absolute URL acceptable as the iframe source on the
|
|
31
|
+
* lightning-embedding wrapper. https is accepted everywhere; plain http is permitted
|
|
32
|
+
* only for localhost / 127.0.0.1 to support local development servers.
|
|
33
|
+
*/
|
|
34
|
+
function isAllowedLightningEmbeddingSrcUrl(src) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(src);
|
|
38
|
+
}
|
|
39
|
+
catch (_a) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (parsed.protocol === 'https:') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (parsed.protocol === 'http:') {
|
|
46
|
+
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=lightningEmbedding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lightningEmbedding.js","sourceRoot":"","sources":["../../src/utils/lightningEmbedding.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AA6BH,8EAcC;AAzCD;;;GAGG;AACU,QAAA,kCAAkC,GAAG;IAChD,aAAa;IACb,cAAc;IACd,wBAAwB;IACxB,oBAAoB;IACpB,cAAc;IACd,gCAAgC;IAChC,oBAAoB;IACpB,mBAAmB;IACnB,eAAe;IACf,yCAAyC;IACzC,sBAAsB;IACtB,yCAAyC;CACjC,CAAC;AAKX;;;;GAIG;AACH,SAAgB,iCAAiC,CAAC,GAAW;IAC3D,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,OAAO,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/lib/utils/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import ApexTriggerGenerator from '../generators/apexTriggerGenerator';
|
|
|
4
4
|
import FlexipageGenerator from '../generators/flexipageGenerator';
|
|
5
5
|
import LightningAppGenerator from '../generators/lightningAppGenerator';
|
|
6
6
|
import LightningComponentGenerator from '../generators/lightningComponentGenerator';
|
|
7
|
+
import LightningEmbeddingGenerator from '../generators/lightningEmbeddingGenerator';
|
|
7
8
|
import LightningEventGenerator from '../generators/lightningEventGenerator';
|
|
8
9
|
import LightningInterfaceGenerator from '../generators/lightningInterfaceGenerator';
|
|
9
10
|
import LightningTestGenerator from '../generators/lightningTestGenerator';
|
|
@@ -26,7 +27,7 @@ export type GeneratorContext = {
|
|
|
26
27
|
/** Absolute path to built-in templates root. Overrides __dirname-based resolution. */
|
|
27
28
|
readonly templatesRootPath?: string;
|
|
28
29
|
};
|
|
29
|
-
export type Generators = typeof AnalyticsTemplateGenerator | typeof ApexClassGenerator | typeof ApexTriggerGenerator | typeof FlexipageGenerator | typeof LightningAppGenerator | typeof LightningComponentGenerator | typeof LightningEventGenerator | typeof LightningTestGenerator | typeof LightningInterfaceGenerator | typeof DigitalExperienceSiteGenerator | typeof ProjectGenerator | typeof StaticResourceGenerator | typeof VisualforceComponentGenerator | typeof VisualforcePageGenerator | typeof UIBundleGenerator;
|
|
30
|
+
export type Generators = typeof AnalyticsTemplateGenerator | typeof ApexClassGenerator | typeof ApexTriggerGenerator | typeof FlexipageGenerator | typeof LightningAppGenerator | typeof LightningComponentGenerator | typeof LightningEventGenerator | typeof LightningTestGenerator | typeof LightningInterfaceGenerator | typeof DigitalExperienceSiteGenerator | typeof LightningEmbeddingGenerator | typeof ProjectGenerator | typeof StaticResourceGenerator | typeof VisualforceComponentGenerator | typeof VisualforcePageGenerator | typeof UIBundleGenerator;
|
|
30
31
|
/**
|
|
31
32
|
* Available Template types
|
|
32
33
|
* Each template type must have a corresponding generator class:
|
|
@@ -45,11 +46,12 @@ export declare enum TemplateType {
|
|
|
45
46
|
LightningInterface = 7,
|
|
46
47
|
LightningTest = 8,
|
|
47
48
|
DigitalExperienceSite = 9,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
LightningEmbedding = 10,
|
|
50
|
+
Project = 11,
|
|
51
|
+
VisualforceComponent = 12,
|
|
52
|
+
VisualforcePage = 13,
|
|
53
|
+
StaticResource = 14,
|
|
54
|
+
UIBundle = 15
|
|
53
55
|
}
|
|
54
56
|
export declare const generators: Map<TemplateType, GeneratorClass<any>>;
|
|
55
57
|
export type CreateOutput = {
|
|
@@ -105,6 +107,13 @@ export interface LightningTestOptions extends TemplateOptions {
|
|
|
105
107
|
testname: string;
|
|
106
108
|
internal: boolean;
|
|
107
109
|
}
|
|
110
|
+
export interface LightningEmbeddingOptions extends TemplateOptions {
|
|
111
|
+
componentname: string;
|
|
112
|
+
src: string;
|
|
113
|
+
sandbox: string;
|
|
114
|
+
shellTitle: string;
|
|
115
|
+
internal: boolean;
|
|
116
|
+
}
|
|
108
117
|
export interface ProjectOptions extends TemplateOptions {
|
|
109
118
|
projectname: string;
|
|
110
119
|
defaultpackagedir: string;
|
package/lib/utils/types.js
CHANGED
|
@@ -13,6 +13,7 @@ const apexTriggerGenerator_1 = require("../generators/apexTriggerGenerator");
|
|
|
13
13
|
const flexipageGenerator_1 = require("../generators/flexipageGenerator");
|
|
14
14
|
const lightningAppGenerator_1 = require("../generators/lightningAppGenerator");
|
|
15
15
|
const lightningComponentGenerator_1 = require("../generators/lightningComponentGenerator");
|
|
16
|
+
const lightningEmbeddingGenerator_1 = require("../generators/lightningEmbeddingGenerator");
|
|
16
17
|
const lightningEventGenerator_1 = require("../generators/lightningEventGenerator");
|
|
17
18
|
const lightningInterfaceGenerator_1 = require("../generators/lightningInterfaceGenerator");
|
|
18
19
|
const lightningTestGenerator_1 = require("../generators/lightningTestGenerator");
|
|
@@ -41,11 +42,12 @@ var TemplateType;
|
|
|
41
42
|
TemplateType[TemplateType["LightningInterface"] = 7] = "LightningInterface";
|
|
42
43
|
TemplateType[TemplateType["LightningTest"] = 8] = "LightningTest";
|
|
43
44
|
TemplateType[TemplateType["DigitalExperienceSite"] = 9] = "DigitalExperienceSite";
|
|
44
|
-
TemplateType[TemplateType["
|
|
45
|
-
TemplateType[TemplateType["
|
|
46
|
-
TemplateType[TemplateType["
|
|
47
|
-
TemplateType[TemplateType["
|
|
48
|
-
TemplateType[TemplateType["
|
|
45
|
+
TemplateType[TemplateType["LightningEmbedding"] = 10] = "LightningEmbedding";
|
|
46
|
+
TemplateType[TemplateType["Project"] = 11] = "Project";
|
|
47
|
+
TemplateType[TemplateType["VisualforceComponent"] = 12] = "VisualforceComponent";
|
|
48
|
+
TemplateType[TemplateType["VisualforcePage"] = 13] = "VisualforcePage";
|
|
49
|
+
TemplateType[TemplateType["StaticResource"] = 14] = "StaticResource";
|
|
50
|
+
TemplateType[TemplateType["UIBundle"] = 15] = "UIBundle";
|
|
49
51
|
})(TemplateType || (exports.TemplateType = TemplateType = {}));
|
|
50
52
|
exports.generators = new Map([
|
|
51
53
|
[TemplateType.AnalyticsTemplate, analyticsTemplateGenerator_1.default],
|
|
@@ -58,6 +60,7 @@ exports.generators = new Map([
|
|
|
58
60
|
[TemplateType.LightningInterface, lightningInterfaceGenerator_1.default],
|
|
59
61
|
[TemplateType.LightningTest, lightningTestGenerator_1.default],
|
|
60
62
|
[TemplateType.DigitalExperienceSite, digitalExperienceSiteGenerator_1.default],
|
|
63
|
+
[TemplateType.LightningEmbedding, lightningEmbeddingGenerator_1.default],
|
|
61
64
|
[TemplateType.Project, projectGenerator_1.default],
|
|
62
65
|
[TemplateType.StaticResource, staticResourceGenerator_1.default],
|
|
63
66
|
[TemplateType.VisualforceComponent, visualforceComponentGenerator_1.default],
|
package/lib/utils/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,yFAAkF;AAClF,yEAAkE;AAClE,6EAAsE;AACtE,yEAAkE;AAClE,+EAAwE;AACxE,2FAAoF;AACpF,mFAA4E;AAC5E,2FAAoF;AACpF,iFAA0E;AAC1E,iGAA0F;AAC1F,qEAA8D;AAC9D,mFAA4E;AAC5E,+FAAwF;AACxF,qFAA8E;AAC9E,uEAAgE;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,yFAAkF;AAClF,yEAAkE;AAClE,6EAAsE;AACtE,yEAAkE;AAClE,+EAAwE;AACxE,2FAAoF;AACpF,2FAAoF;AACpF,mFAA4E;AAC5E,2FAAoF;AACpF,iFAA0E;AAC1E,iGAA0F;AAC1F,qEAA8D;AAC9D,mFAA4E;AAC5E,+FAAwF;AACxF,qFAA8E;AAC9E,uEAAgE;AAuChE;;;;;;GAMG;AACH,IAAY,YAiBX;AAjBD,WAAY,YAAY;IACtB,yEAAiB,CAAA;IACjB,yDAAS,CAAA;IACT,6DAAW,CAAA;IACX,yDAAS,CAAA;IACT,+DAAY,CAAA;IACZ,2EAAkB,CAAA;IAClB,mEAAc,CAAA;IACd,2EAAkB,CAAA;IAClB,iEAAa,CAAA;IACb,iFAAqB,CAAA;IACrB,4EAAkB,CAAA;IAClB,sDAAO,CAAA;IACP,gFAAoB,CAAA;IACpB,sEAAe,CAAA;IACf,oEAAc,CAAA;IACd,wDAAQ,CAAA;AACV,CAAC,EAjBW,YAAY,4BAAZ,YAAY,QAiBvB;AAEY,QAAA,UAAU,GAAG,IAAI,GAAG,CAAoC;IACnE,CAAC,YAAY,CAAC,iBAAiB,EAAE,oCAA0B,CAAC;IAC5D,CAAC,YAAY,CAAC,SAAS,EAAE,4BAAkB,CAAC;IAC5C,CAAC,YAAY,CAAC,WAAW,EAAE,8BAAoB,CAAC;IAChD,CAAC,YAAY,CAAC,SAAS,EAAE,4BAAkB,CAAC;IAC5C,CAAC,YAAY,CAAC,YAAY,EAAE,+BAAqB,CAAC;IAClD,CAAC,YAAY,CAAC,kBAAkB,EAAE,qCAA2B,CAAC;IAC9D,CAAC,YAAY,CAAC,cAAc,EAAE,iCAAuB,CAAC;IACtD,CAAC,YAAY,CAAC,kBAAkB,EAAE,qCAA2B,CAAC;IAC9D,CAAC,YAAY,CAAC,aAAa,EAAE,gCAAsB,CAAC;IACpD,CAAC,YAAY,CAAC,qBAAqB,EAAE,wCAA8B,CAAC;IACpE,CAAC,YAAY,CAAC,kBAAkB,EAAE,qCAA2B,CAAC;IAC9D,CAAC,YAAY,CAAC,OAAO,EAAE,0BAAgB,CAAC;IACxC,CAAC,YAAY,CAAC,cAAc,EAAE,iCAAuB,CAAC;IACtD,CAAC,YAAY,CAAC,oBAAoB,EAAE,uCAA6B,CAAC;IAClE,CAAC,YAAY,CAAC,eAAe,EAAE,kCAAwB,CAAC;IACxD,CAAC,YAAY,CAAC,QAAQ,EAAE,2BAAiB,CAAC;CAC3C,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/templates",
|
|
3
|
-
"version": "66.
|
|
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": "^
|
|
29
|
-
"@salesforce/ui-bundle-template-app-react-template-b2x": "^
|
|
30
|
-
"@salesforce/ui-bundle-template-base-react-app": "^
|
|
31
|
-
"@salesforce/ui-bundle-template-base-web-app": "^
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"eslint-plugin-jsdoc": "^46.10.1",
|
|
51
51
|
"eslint-plugin-prettier": "^3.1.3",
|
|
52
52
|
"husky": "^8.0.3",
|
|
53
|
-
"mocha": "^11.7.
|
|
53
|
+
"mocha": "^11.7.6",
|
|
54
54
|
"nyc": "^15.1.0",
|
|
55
55
|
"prettier": "^2.0.5",
|
|
56
56
|
"pretty-quick": "^3.3.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
|
-
}
|