@salesforce/templates 66.4.1 → 66.4.2

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 (81) hide show
  1. package/lib/generators/projectGenerator.js +30 -1
  2. package/lib/generators/projectGenerator.js.map +1 -1
  3. package/lib/templates/project/nativemobile/README.md +21 -0
  4. package/lib/templates/project/nativemobile/ScratchDef.json +5 -0
  5. package/lib/templates/project/nativemobile/appMetadata-content.json +29 -0
  6. package/lib/templates/project/nativemobile/appMetadata-meta.json +5 -0
  7. package/lib/templates/project/nativemobile/buildMetadata-content.json +8 -0
  8. package/lib/templates/project/nativemobile/buildMetadata-meta.json +5 -0
  9. package/lib/templates/project/nativemobile/digitalExperience-meta.xml +4 -0
  10. package/lib/templates/project/nativemobile/ecDefinition-content.json +5 -0
  11. package/lib/templates/project/nativemobile/ecDefinition-meta.json +5 -0
  12. package/lib/templates/project/nativemobile/homeScreen-content.json +77 -0
  13. package/lib/templates/project/nativemobile/homeScreen-meta.json +5 -0
  14. package/lib/templates/project/reactb2e/AGENT.md +17 -13
  15. package/lib/templates/project/reactb2e/CHANGELOG.md +276 -0
  16. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/index.html +2 -2
  17. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/package.json +10 -5
  18. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/src/app.tsx +1 -9
  19. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +15 -5
  20. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/tsconfig.tsbuildinfo +1 -0
  21. package/lib/templates/project/reactb2e/_p_/_m_/_w_/_a_/vite.config.ts +1 -2
  22. package/lib/templates/project/reactb2e/_r_/skills/feature-graphql-graphql-data-access/docs/generate-mutation-query.md +11 -5
  23. package/lib/templates/project/reactb2e/_r_/skills/feature-graphql-graphql-data-access/docs/generate-read-query.md +13 -6
  24. package/lib/templates/project/reactb2e/_r_/skills/feature-react-file-upload-file-upload/SKILL.md +396 -0
  25. package/lib/templates/project/reactb2e/_r_/skills/webapp-features/SKILL.md +210 -0
  26. package/lib/templates/project/reactb2e/_r_/skills/{webapp-react-add-component → webapp-react}/SKILL.md +5 -3
  27. package/lib/templates/project/reactb2e/_r_/skills/{webapp-react-add-component → webapp-react}/implementation/header-footer.md +8 -0
  28. package/lib/templates/project/{reactb2x/_r_/skills/webapp-react-add-component → reactb2e/_r_/skills/webapp-react}/implementation/page.md +8 -7
  29. package/lib/templates/project/reactb2e/_r_/skills/webapp-ui-ux/SKILL.md +11 -8
  30. package/lib/templates/project/reactb2e/_r_/webapp-cli-commands.md +88 -0
  31. package/lib/templates/project/reactb2e/_r_/webapp-react-code-quality.md +1 -1
  32. package/lib/templates/project/reactb2e/_r_/webapp-react-typescript.md +1 -1
  33. package/lib/templates/project/reactb2e/_r_/webapp-react.md +55 -1
  34. package/lib/templates/project/reactb2e/_r_/webapp-skills-first.md +1 -1
  35. package/lib/templates/project/reactb2e/_r_/webapp-webapplication.md +159 -0
  36. package/lib/templates/project/reactb2e/_r_/webapp.md +2 -2
  37. package/lib/templates/project/reactb2e/package.json +1 -1
  38. package/lib/templates/project/reactb2e/scripts/prepare-import-unique-fields.js +17 -3
  39. package/lib/templates/project/reactb2e/scripts/setup-cli.mjs +318 -67
  40. package/lib/templates/project/reactb2x/AGENT.md +17 -13
  41. package/lib/templates/project/reactb2x/CHANGELOG.md +276 -0
  42. package/lib/templates/project/reactb2x/_p_/_m_/_d_/_s_/_a1_/sfdc_cms__site/appreacttemplateb2x1/content.json +1 -1
  43. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/index.html +2 -2
  44. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/package.json +10 -4
  45. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/api/userProfileApi.ts +15 -1
  46. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ChangePassword.tsx +1 -1
  47. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ForgotPassword.tsx +1 -1
  48. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +2 -2
  49. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +2 -2
  50. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ResetPassword.tsx +1 -1
  51. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/src/app.tsx +2 -1
  52. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/tsconfig.tsbuildinfo +1 -0
  53. package/lib/templates/project/reactb2x/_p_/_m_/_w_/_a_/vite.config.ts +1 -2
  54. package/lib/templates/project/reactb2x/_r_/skills/feature-graphql-graphql-data-access/docs/generate-mutation-query.md +11 -5
  55. package/lib/templates/project/reactb2x/_r_/skills/feature-graphql-graphql-data-access/docs/generate-read-query.md +13 -6
  56. package/lib/templates/project/reactb2x/_r_/skills/feature-react-file-upload-file-upload/SKILL.md +396 -0
  57. package/lib/templates/project/reactb2x/_r_/skills/webapp-features/SKILL.md +210 -0
  58. package/lib/templates/project/reactb2x/_r_/skills/{webapp-react-add-component → webapp-react}/SKILL.md +5 -3
  59. package/lib/templates/project/reactb2x/_r_/skills/{webapp-react-add-component → webapp-react}/implementation/header-footer.md +8 -0
  60. package/lib/templates/project/{reactb2e/_r_/skills/webapp-react-add-component → reactb2x/_r_/skills/webapp-react}/implementation/page.md +8 -7
  61. package/lib/templates/project/reactb2x/_r_/skills/webapp-ui-ux/SKILL.md +11 -8
  62. package/lib/templates/project/reactb2x/_r_/webapp-cli-commands.md +88 -0
  63. package/lib/templates/project/reactb2x/_r_/webapp-react-code-quality.md +1 -1
  64. package/lib/templates/project/reactb2x/_r_/webapp-react-typescript.md +1 -1
  65. package/lib/templates/project/reactb2x/_r_/webapp-react.md +55 -1
  66. package/lib/templates/project/reactb2x/_r_/webapp-skills-first.md +1 -1
  67. package/lib/templates/project/reactb2x/_r_/webapp-webapplication.md +159 -0
  68. package/lib/templates/project/reactb2x/_r_/webapp.md +2 -2
  69. package/lib/templates/project/reactb2x/package.json +1 -1
  70. package/lib/templates/project/reactb2x/scripts/prepare-import-unique-fields.js +17 -3
  71. package/lib/templates/project/reactb2x/scripts/setup-cli.mjs +318 -67
  72. package/lib/templates/webapplication/reactbasic/index.html +2 -2
  73. package/lib/templates/webapplication/reactbasic/package.json +3 -3
  74. package/lib/templates/webapplication/reactbasic/src/app.tsx +1 -9
  75. package/lib/templates/webapplication/reactbasic/vite.config.ts +1 -2
  76. package/lib/utils/types.d.ts +1 -1
  77. package/package.json +6 -6
  78. package/lib/templates/project/reactb2e/_r_/webapp-no-node-e.md +0 -65
  79. package/lib/templates/project/reactb2x/_r_/webapp-no-node-e.md +0 -65
  80. /package/lib/templates/project/reactb2e/_r_/skills/{webapp-react-add-component → webapp-react}/implementation/component.md +0 -0
  81. /package/lib/templates/project/reactb2x/_r_/skills/{webapp-react-add-component → webapp-react}/implementation/component.md +0 -0
@@ -15,11 +15,16 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/agentforce-conversation-client": "^1.84.0",
19
- "@salesforce/sdk-data": "^1.84.0",
20
- "@salesforce/webapp-experimental": "^1.84.0",
18
+ "@salesforce/agentforce-conversation-client": "file:../../../../../../../../../agentforceConversationClient",
19
+ "@salesforce/micro-frontends-experimental": "file:../../../../../../../../../micro-frontends",
20
+ "@salesforce/sdk-chat": "file:../../../../../../../../../sdk/sdk-chat",
21
+ "@salesforce/sdk-core": "file:../../../../../../../../../sdk/sdk-core",
22
+ "@salesforce/sdk-data": "file:../../../../../../../../../sdk/sdk-data",
23
+ "@salesforce/sdk-lightning": "file:../../../../../../../../../sdk/sdk-lightning",
24
+ "@salesforce/sdk-view": "file:../../../../../../../../../sdk/sdk-view",
25
+ "@salesforce/webapp-experimental": "file:../../../../../../../../../webapps",
21
26
  "@tailwindcss/vite": "^4.1.17",
22
- "@tanstack/react-form": "^1.28.4",
27
+ "@tanstack/react-form": "^1.28.5",
23
28
  "class-variance-authority": "^0.7.1",
24
29
  "clsx": "^2.1.1",
25
30
  "lucide-react": "^0.562.0",
@@ -41,7 +46,7 @@
41
46
  "@graphql-eslint/eslint-plugin": "^4.1.0",
42
47
  "@graphql-tools/utils": "^11.0.0",
43
48
  "@playwright/test": "^1.49.0",
44
- "@salesforce/vite-plugin-webapp-experimental": "^1.84.0",
49
+ "@salesforce/vite-plugin-webapp-experimental": "file:../../../../../../../../../vite-plugin-webapps",
45
50
  "@testing-library/jest-dom": "^6.6.3",
46
51
  "@testing-library/react": "^16.1.0",
47
52
  "@testing-library/user-event": "^14.5.2",
@@ -4,15 +4,7 @@ import { StrictMode } from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
5
  import './styles/global.css';
6
6
 
7
- // Match Vite base so client-side routes work when deployed under a path (e.g. /lwr/application/ai/c-webapp2/).
8
- // When served at root (e.g. e2e with static serve), use '/' so routes match.
9
- const base = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '') || '/';
10
- const basename =
11
- typeof window !== 'undefined' &&
12
- (window.location.pathname === '/' ||
13
- !window.location.pathname.startsWith('/lwr/'))
14
- ? '/'
15
- : base;
7
+ const basename = (globalThis as any).SFDC_ENV?.basePath;
16
8
  const router = createBrowserRouter(routes, { basename });
17
9
 
18
10
  createRoot(document.getElementById('root')!).render(
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { embedAgentforceClient } from "@salesforce/agentforce-conversation-client";
8
- import { useEffect } from "react";
8
+ import { useEffect, useRef } from "react";
9
9
  import type {
10
10
  ResolvedEmbedOptions,
11
11
  AgentforceConversationClientProps,
@@ -57,14 +57,20 @@ export function AgentforceConversationClient({
57
57
  salesforceOrigin,
58
58
  frontdoorUrl,
59
59
  }: AgentforceConversationClientProps) {
60
+ const containerRef = useRef<HTMLDivElement>(null);
61
+ const inline = agentforceClientConfig?.renderingConfig?.mode === "inline";
62
+
60
63
  useEffect(() => {
61
64
  const singleton = getSingleton();
62
65
  if (singleton.initialized || singleton.initPromise) {
63
66
  return;
64
67
  }
65
68
 
69
+ if (inline && !containerRef.current) {
70
+ return;
71
+ }
72
+
66
73
  const initialize = (options: ResolvedEmbedOptions) => {
67
- // If already initialized while this flow was in progress, no-op.
68
74
  if (singleton.initialized) {
69
75
  return;
70
76
  }
@@ -73,7 +79,7 @@ export function AgentforceConversationClient({
73
79
  singleton.initialized = true;
74
80
  return;
75
81
  }
76
- const host = getOrCreateGlobalHost();
82
+ const host = inline ? containerRef.current! : getOrCreateGlobalHost();
77
83
 
78
84
  embedAgentforceClient({
79
85
  container: host,
@@ -119,9 +125,13 @@ export function AgentforceConversationClient({
119
125
  // Intentionally no cleanup:
120
126
  // This component guarantees a single LO initialization per window.
121
127
  };
122
- }, [salesforceOrigin, frontdoorUrl, agentforceClientConfig]);
128
+ }, [salesforceOrigin, frontdoorUrl, agentforceClientConfig, inline]);
129
+
130
+ if (!inline) {
131
+ return null;
132
+ }
123
133
 
124
- return null;
134
+ return <div ref={containerRef} />;
125
135
  }
126
136
 
127
137
  export default AgentforceConversationClient;
@@ -0,0 +1 @@
1
+ {"root":["./src/app.tsx","./src/appLayout.tsx","./src/index.ts","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/components/AgentforceConversationClient.tsx","./src/components/__inherit_AgentforceConversationClient.tsx","./src/components/alerts/status-alert.tsx","./src/components/layouts/card-layout.tsx","./src/components/ui/alert.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/features/global-search/constants.ts","./src/features/global-search/api/objectDetailService.ts","./src/features/global-search/api/objectInfoGraphQLService.ts","./src/features/global-search/api/objectInfoService.ts","./src/features/global-search/api/recordListGraphQLService.ts","./src/features/global-search/components/detail/DetailFields.tsx","./src/features/global-search/components/detail/DetailForm.tsx","./src/features/global-search/components/detail/DetailHeader.tsx","./src/features/global-search/components/detail/DetailLayoutSections.tsx","./src/features/global-search/components/detail/Section.tsx","./src/features/global-search/components/detail/SectionRow.tsx","./src/features/global-search/components/detail/UiApiDetailForm.tsx","./src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx","./src/features/global-search/components/detail/formatted/FormattedAddress.tsx","./src/features/global-search/components/detail/formatted/FormattedEmail.tsx","./src/features/global-search/components/detail/formatted/FormattedPhone.tsx","./src/features/global-search/components/detail/formatted/FormattedText.tsx","./src/features/global-search/components/detail/formatted/FormattedUrl.tsx","./src/features/global-search/components/filters/FilterField.tsx","./src/features/global-search/components/filters/FilterInput.tsx","./src/features/global-search/components/filters/FilterSelect.tsx","./src/features/global-search/components/filters/FiltersPanel.tsx","./src/features/global-search/components/forms/filters-form.tsx","./src/features/global-search/components/forms/submit-button.tsx","./src/features/global-search/components/search/GlobalSearchInput.tsx","./src/features/global-search/components/search/ResultCardFields.tsx","./src/features/global-search/components/search/SearchHeader.tsx","./src/features/global-search/components/search/SearchPagination.tsx","./src/features/global-search/components/search/SearchResultCard.tsx","./src/features/global-search/components/search/SearchResultsPanel.tsx","./src/features/global-search/components/shared/LoadingFallback.tsx","./src/features/global-search/filters/FilterInput.tsx","./src/features/global-search/filters/FilterSelect.tsx","./src/features/global-search/hooks/form.tsx","./src/features/global-search/hooks/useObjectInfoBatch.ts","./src/features/global-search/hooks/useObjectSearchData.ts","./src/features/global-search/hooks/useRecordDetailLayout.ts","./src/features/global-search/hooks/useRecordListGraphQL.ts","./src/features/global-search/pages/DetailPage.tsx","./src/features/global-search/pages/GlobalSearch.tsx","./src/features/global-search/types/schema.d.ts","./src/features/global-search/types/filters/filters.ts","./src/features/global-search/types/filters/picklist.ts","./src/features/global-search/types/objectInfo/objectInfo.ts","./src/features/global-search/types/recordDetail/recordDetail.ts","./src/features/global-search/types/search/searchResults.ts","./src/features/global-search/utils/apiUtils.ts","./src/features/global-search/utils/cacheUtils.ts","./src/features/global-search/utils/debounce.ts","./src/features/global-search/utils/fieldUtils.ts","./src/features/global-search/utils/fieldValueExtractor.ts","./src/features/global-search/utils/filterUtils.ts","./src/features/global-search/utils/formDataTransformUtils.ts","./src/features/global-search/utils/formUtils.ts","./src/features/global-search/utils/graphQLNodeFieldUtils.ts","./src/features/global-search/utils/graphQLObjectInfoAdapter.ts","./src/features/global-search/utils/graphQLRecordAdapter.ts","./src/features/global-search/utils/layoutTransformUtils.ts","./src/features/global-search/utils/linkUtils.ts","./src/features/global-search/utils/paginationUtils.ts","./src/features/global-search/utils/recordUtils.ts","./src/features/global-search/utils/sanitizationUtils.ts","./src/lib/utils.ts","./src/pages/Home.tsx","./src/pages/NotFound.tsx","./src/pages/TestAccPage.tsx","./src/types/conversation.ts","./vite-env.d.ts","./vitest-env.d.ts"],"version":"5.9.3"}
@@ -8,8 +8,7 @@ import codegen from 'vite-plugin-graphql-codegen';
8
8
 
9
9
  export default defineConfig(({ mode }) => {
10
10
  return {
11
- // Ensure root base for e2e/static serve; plugin may override when deployed under a path
12
- base: '/',
11
+ base: './',
13
12
  // Type assertion avoids Plugin type mismatch when dist has its own node_modules (vite/rollup)
14
13
  plugins: [
15
14
  tailwindcss(),
@@ -173,15 +173,21 @@ const QUERY_VARIABLES = {
173
173
 
174
174
  **Workflow**
175
175
 
176
- 1. **Report Step** - Explain that you are able to test the query using the same method used during introspection
177
- 1. You **MUST** report the method you will use, based on the one you used during schema exploration
178
- 2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method
176
+ 1. **Report Step** - Explain that you are able to test the query using `sf api request rest`
177
+ 2. **Interactive Step** - Ask the user whether they want you to test the query
179
178
  1. **WAIT** for the user's answer.
180
179
  3. **Input Arguments** - You **MUST** ask the user for the input arguments to use
181
180
  1. **WAIT** for the user's answer.
182
181
  4. **Test Query** - If the user are OK with you testing the query:
183
- 1. Use the selected method to test the query
184
- 2. **IMPORTANT** - If you use the Salesforce CLI `sf api request graphql` command, you will need to inject the variable values directly into the query, as this command doesn't accept variables as a parameter
182
+ 1. Use `sf api request rest` to POST the query and variables to the GraphQL endpoint:
183
+ ```bash
184
+ sf api request rest /services/data/v65.0/graphql \
185
+ --method POST \
186
+ --body '{"query":"mutation mutateEntity($input: EntityNameOperationInput!) { uiapi { EntityNameOperation(input: $input) { Record { Id } } } }","variables":{"input":{"EntityName":{"Field":"Value"}}}}'
187
+ ```
188
+ 2. Replace `v65.0` with the API version of the target org
189
+ 3. Replace the `query` value with the generated mutation query string
190
+ 4. Replace the `variables` value with the user-provided input arguments
185
191
  5. **Result Analysis** - Retrieve the `data` and `errors` attributes from the returned payload, and report the result of the test as one of the following options:
186
192
  1. `PARTIAL` if `data` is not an empty object, but `errors` is not an empty list - Explanation: some of the queried fields are not accessible on mutations
187
193
  2. `FAILED` if `data` is an empty object - Explanation: the query is not valid
@@ -168,14 +168,21 @@ const QUERY_VARIABLES = {
168
168
 
169
169
  **Workflow**
170
170
 
171
- 1. **Report Step** - Explain that you are able to test the query using the same method used during introspection
172
- 1. You **MUST** report the method you will use, based on the one you used during schema exploration
173
- 2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method
171
+ 1. **Report Step** - Explain that you are able to test the query using `sf api request rest`
172
+ 2. **Interactive Step** - Ask the user whether they want you to test the query
174
173
  1. **WAIT** for the user's answer.
175
174
  3. **Test Query** - If the user are OK with you testing the query:
176
- 1. Use the selected method to test the query
177
- 2. Report the result of the test as `SUCCESS` if the query executed without error, or `FAILED` if you got errors
178
- 3. If the query executed without any errors, but you received no data, then the query is valid, and the result of the test is `SUCCESS`
175
+ 1. Use `sf api request rest` to POST the query to the GraphQL endpoint:
176
+ ```bash
177
+ sf api request rest /services/data/v65.0/graphql \
178
+ --method POST \
179
+ --body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}'
180
+ ```
181
+ 2. Replace `v65.0` with the API version of the target org
182
+ 3. Replace the `query` value with the generated read query string
183
+ 4. If the query uses variables, include them in the JSON body as a `variables` key
184
+ 5. Report the result of the test as `SUCCESS` if the query executed without error, or `FAILED` if you got errors
185
+ 6. If the query executed without any errors, but you received no data, then the query is valid, and the result of the test is `SUCCESS`
179
186
  4. **Remediation Step** - If status is `FAILED`, use the [`FAILED` status handling workflows](#failed-status-handling-workflow)
180
187
 
181
188
  ### `FAILED` Status Handling Workflow
@@ -0,0 +1,396 @@
1
+ ---
2
+ name: feature-react-file-upload-file-upload
3
+ description: Add file upload functionality to React webapps with progress tracking and Salesforce ContentVersion integration. Use when the user wants to upload files, attach documents, handle file input, create file dropzones, track upload progress, or link files to Salesforce records. This feature provides programmatic APIs ONLY — no components or hooks are exported. Build your own custom UI using the upload() API. ALWAYS use this feature instead of building file upload from scratch with FormData or XHR.
4
+ ---
5
+
6
+ # File Upload API (workflow)
7
+
8
+ When the user wants file upload functionality in a React webapp, follow this workflow. This feature provides **APIs only** — you must build the UI components yourself using the provided APIs.
9
+
10
+ ## CRITICAL: This is an API-only package
11
+
12
+ The package exports **programmatic APIs**, not React components or hooks. You will:
13
+
14
+ - Use the `upload()` function to handle file uploads with progress tracking
15
+ - Build your own custom UI (file input, dropzone, progress bars, etc.)
16
+ - Track upload progress through the `onProgress` callback
17
+
18
+ **Do NOT:**
19
+
20
+ - Expect pre-built components like `<FileUpload />` — they are not exported
21
+ - Try to import React hooks like `useFileUpload` — they are not exported
22
+ - Look for dropzone components — they are not exported
23
+
24
+ The source code contains reference components for demonstration, but they are **not available** as imports. Use them as examples to build your own UI.
25
+
26
+ ## 1. Install the package
27
+
28
+ ```bash
29
+ npm install @salesforce/webapp-template-feature-react-file-upload-experimental
30
+ ```
31
+
32
+ Dependencies are automatically installed:
33
+
34
+ - `@salesforce/webapp-experimental` (API client)
35
+ - `@salesforce/sdk-data` (data SDK)
36
+
37
+ ## 2. Understand the three upload patterns
38
+
39
+ ### Pattern A: Basic upload (no record linking)
40
+
41
+ Upload files to Salesforce and get back `contentBodyId` for each file. No ContentVersion record is created.
42
+
43
+ **When to use:**
44
+
45
+ - User wants to upload files first, then create/link them to a record later
46
+ - Building a multi-step form where the record doesn't exist yet
47
+ - Deferred record linking scenarios
48
+
49
+ ```tsx
50
+ import { upload } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
51
+
52
+ const results = await upload({
53
+ files: [file1, file2],
54
+ onProgress: (progress) => {
55
+ console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
56
+ },
57
+ });
58
+
59
+ // results[0].contentBodyId: "069..." (always available)
60
+ // results[0].contentVersionId: undefined (no record linked)
61
+ ```
62
+
63
+ ### Pattern B: Upload with immediate record linking
64
+
65
+ Upload files and immediately link them to an existing Salesforce record by creating ContentVersion records.
66
+
67
+ **When to use:**
68
+
69
+ - Record already exists (Account, Opportunity, Case, etc.)
70
+ - User wants files immediately attached to the record
71
+ - Direct upload-and-attach scenarios
72
+
73
+ ```tsx
74
+ import { upload } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
75
+
76
+ const results = await upload({
77
+ files: [file1, file2],
78
+ recordId: "001xx000000yyyy", // Existing record ID
79
+ onProgress: (progress) => {
80
+ console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
81
+ },
82
+ });
83
+
84
+ // results[0].contentBodyId: "069..." (always available)
85
+ // results[0].contentVersionId: "068..." (linked to record)
86
+ ```
87
+
88
+ ### Pattern C: Deferred record linking (record creation flow)
89
+
90
+ Upload files without a record, then link them after the record is created.
91
+
92
+ **When to use:**
93
+
94
+ - Building a "create record with attachments" form
95
+ - Record doesn't exist until form submission
96
+ - Need to upload files before knowing the final record ID
97
+
98
+ ```tsx
99
+ import {
100
+ upload,
101
+ createContentVersion,
102
+ } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
103
+
104
+ // Step 1: Upload files (no recordId)
105
+ const uploadResults = await upload({
106
+ files: [file1, file2],
107
+ onProgress: (progress) => console.log(progress),
108
+ });
109
+
110
+ // Step 2: Create the record
111
+ const newRecordId = await createRecord(formData);
112
+
113
+ // Step 3: Link uploaded files to the new record
114
+ for (const file of uploadResults) {
115
+ const contentVersionId = await createContentVersion(
116
+ new File([""], file.fileName),
117
+ file.contentBodyId,
118
+ newRecordId,
119
+ );
120
+ }
121
+ ```
122
+
123
+ ## 3. Build your custom UI
124
+
125
+ The package provides the backend — you build the frontend. Here's a minimal example:
126
+
127
+ ```tsx
128
+ import {
129
+ upload,
130
+ type FileUploadProgress,
131
+ } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
132
+ import { useState } from "react";
133
+
134
+ function CustomFileUpload({ recordId }: { recordId?: string }) {
135
+ const [progress, setProgress] = useState<Map<string, FileUploadProgress>>(new Map());
136
+
137
+ const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
138
+ const files = Array.from(event.target.files || []);
139
+
140
+ await upload({
141
+ files,
142
+ recordId,
143
+ onProgress: (fileProgress) => {
144
+ setProgress((prev) => new Map(prev).set(fileProgress.fileName, fileProgress));
145
+ },
146
+ });
147
+ };
148
+
149
+ return (
150
+ <div>
151
+ <input type="file" multiple onChange={handleFileSelect} />
152
+
153
+ {Array.from(progress.entries()).map(([fileName, fileProgress]) => (
154
+ <div key={fileName}>
155
+ {fileName}: {fileProgress.status} - {fileProgress.progress}%
156
+ {fileProgress.error && <span>Error: {fileProgress.error}</span>}
157
+ </div>
158
+ ))}
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ## 4. Track upload progress
165
+
166
+ The `onProgress` callback fires multiple times for each file as it moves through stages:
167
+
168
+ | Status | When | Progress Value |
169
+ | -------------- | ---------------------------------------------- | -------------------- |
170
+ | `"pending"` | File queued for upload | `0` |
171
+ | `"uploading"` | Upload in progress (XHR) | `0-100` (percentage) |
172
+ | `"processing"` | Creating ContentVersion (if recordId provided) | `0` |
173
+ | `"success"` | Upload complete | `100` |
174
+ | `"error"` | Upload failed | `0` |
175
+
176
+ **Always provide visual feedback:**
177
+
178
+ - Show file name
179
+ - Display current status
180
+ - Render progress bar for "uploading" status
181
+ - Show error message if status is "error"
182
+
183
+ ## 5. Cancel uploads (optional)
184
+
185
+ Use an `AbortController` to allow users to cancel uploads:
186
+
187
+ ```tsx
188
+ const abortController = new AbortController();
189
+
190
+ const handleUpload = async (files: File[]) => {
191
+ try {
192
+ await upload({
193
+ files,
194
+ signal: abortController.signal,
195
+ onProgress: (progress) => console.log(progress),
196
+ });
197
+ } catch (error) {
198
+ console.error("Upload cancelled or failed:", error);
199
+ }
200
+ };
201
+
202
+ const cancelUpload = () => {
203
+ abortController.abort();
204
+ };
205
+ ```
206
+
207
+ ## 6. Link to current user (special case)
208
+
209
+ If the user wants to upload files to their own profile or personal library:
210
+
211
+ ```tsx
212
+ import {
213
+ upload,
214
+ getCurrentUserId,
215
+ } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
216
+
217
+ const userId = await getCurrentUserId();
218
+ await upload({ files, recordId: userId });
219
+ ```
220
+
221
+ ## API Reference
222
+
223
+ ### upload(options)
224
+
225
+ Main upload API that handles complete flow with progress tracking.
226
+
227
+ ```typescript
228
+ interface UploadOptions {
229
+ files: File[];
230
+ recordId?: string | null; // If provided, creates ContentVersion
231
+ onProgress?: (progress: FileUploadProgress) => void;
232
+ signal?: AbortSignal; // Optional cancellation
233
+ }
234
+
235
+ interface FileUploadProgress {
236
+ fileName: string;
237
+ status: "pending" | "uploading" | "processing" | "success" | "error";
238
+ progress: number; // 0-100 for uploading, 0 for other states
239
+ error?: string;
240
+ }
241
+
242
+ interface FileUploadResult {
243
+ fileName: string;
244
+ size: number;
245
+ contentBodyId: string; // Always available
246
+ contentVersionId?: string; // Only if recordId was provided
247
+ }
248
+ ```
249
+
250
+ **Returns:** `Promise<FileUploadResult[]>`
251
+
252
+ ### createContentVersion(file, contentBodyId, recordId)
253
+
254
+ Manually create a ContentVersion record from a previously uploaded file.
255
+
256
+ ```typescript
257
+ async function createContentVersion(
258
+ file: File,
259
+ contentBodyId: string,
260
+ recordId: string,
261
+ ): Promise<string | undefined>;
262
+ ```
263
+
264
+ **Parameters:**
265
+
266
+ - `file` — File object (used for metadata like name)
267
+ - `contentBodyId` — ContentBody ID from previous upload
268
+ - `recordId` — Record ID for FirstPublishLocationId
269
+
270
+ **Returns:** ContentVersion ID if successful
271
+
272
+ ### getCurrentUserId()
273
+
274
+ Get the current user's Salesforce ID.
275
+
276
+ ```typescript
277
+ async function getCurrentUserId(): Promise<string>;
278
+ ```
279
+
280
+ **Returns:** Current user ID
281
+
282
+ ## Common UI patterns
283
+
284
+ ### File input with button
285
+
286
+ ```tsx
287
+ <input type="file" multiple accept=".pdf,.doc,.docx,.jpg,.png" onChange={handleFileSelect} />
288
+ ```
289
+
290
+ ### Drag-and-drop zone
291
+
292
+ Build your own dropzone using native events:
293
+
294
+ ```tsx
295
+ function DropZone({ onDrop }: { onDrop: (files: File[]) => void }) {
296
+ const handleDrop = (e: React.DragEvent) => {
297
+ e.preventDefault();
298
+ const files = Array.from(e.dataTransfer.files);
299
+ onDrop(files);
300
+ };
301
+
302
+ return (
303
+ <div
304
+ onDrop={handleDrop}
305
+ onDragOver={(e) => e.preventDefault()}
306
+ style={{ border: "2px dashed #ccc", padding: "2rem" }}
307
+ >
308
+ Drop files here
309
+ </div>
310
+ );
311
+ }
312
+ ```
313
+
314
+ ### Progress bar
315
+
316
+ ```tsx
317
+ {
318
+ progress.status === "uploading" && (
319
+ <div style={{ width: "100%", background: "#eee" }}>
320
+ <div
321
+ style={{
322
+ width: `${progress.progress}%`,
323
+ background: "#0176d3",
324
+ height: "8px",
325
+ }}
326
+ />
327
+ </div>
328
+ );
329
+ }
330
+ ```
331
+
332
+ ## Decision tree for agents
333
+
334
+ **User asks for file upload functionality:**
335
+
336
+ 1. **Ask about record context:**
337
+ - "Do you want to link uploaded files to a specific record, or upload them first and link later?"
338
+
339
+ 2. **Based on response:**
340
+ - **Link to existing record** → Use Pattern B with `recordId`
341
+ - **Upload first, link later** → Use Pattern A (no recordId), then Pattern C for linking
342
+ - **Link to current user** → Use Pattern B with `getCurrentUserId()`
343
+
344
+ 3. **Build the UI:**
345
+ - Create file input or dropzone (not provided by package)
346
+ - Add progress display for each file (status + progress bar)
347
+ - Handle errors in the UI
348
+
349
+ 4. **Test the implementation:**
350
+ - Verify progress callbacks fire correctly
351
+ - Check that `contentBodyId` is returned
352
+ - If `recordId` was provided, verify `contentVersionId` is returned
353
+
354
+ ## Reference implementation
355
+
356
+ The package includes a reference implementation in `src/features/fileupload/` with:
357
+
358
+ - `FileUpload.tsx` — Complete component with dropzone and dialog
359
+ - `FileUploadDialog.tsx` — Progress tracking dialog
360
+ - `FileUploadDropZone.tsx` — Drag-and-drop zone
361
+ - `useFileUpload.ts` — React hook for state management
362
+
363
+ **These are NOT exported** but can be viewed as examples. Read the source files to understand patterns for building your own UI.
364
+
365
+ ## Troubleshooting
366
+
367
+ **Upload fails with CORS error:**
368
+
369
+ - Ensure the webapp is properly deployed to Salesforce or running on `localhost`
370
+ - Check that the org allows the origin in CORS settings
371
+
372
+ **No progress updates:**
373
+
374
+ - Verify `onProgress` callback is provided
375
+ - Check that the callback function updates React state correctly
376
+
377
+ **ContentVersion not created:**
378
+
379
+ - Verify `recordId` is provided to `upload()` function
380
+ - Check that the record ID is valid and exists in the org
381
+ - Ensure user has permissions to create ContentVersion records
382
+
383
+ **Files upload but don't appear in record:**
384
+
385
+ - Verify `recordId` is correct
386
+ - Check that ContentVersion was created (look for `contentVersionId` in results)
387
+ - Confirm user has access to view files on the record
388
+
389
+ ## DO NOT do these things
390
+
391
+ - ❌ Build XHR/fetch upload logic from scratch — use the `upload()` API
392
+ - ❌ Try to import `<FileUpload />` component — it's not exported
393
+ - ❌ Try to import `useFileUpload` hook — it's not exported
394
+ - ❌ Use third-party file upload libraries when this feature exists
395
+ - ❌ Skip progress tracking — always provide user feedback
396
+ - ❌ Ignore errors — always handle and display error messages