mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-05 06:41:42 +08:00
feat: syncing webapp skills sync to afv @W-21338965@ (#57)
* feat: removing old webapp skills * feat: adding sync of skills from webapps to afv * feat: adding the first iteration of skills * feat: pin template deps to latest npm versions and flatten skill folders - Add pin-template-deps.js to resolve "*" deps to exact npm versions - Integrate pinning into sync-template-skills npm script - Remove check-template-skills-versions.js (no longer needed) - Simplify workflow to single sync step - Flatten skill output: one folder per skill with cleaned names Made-with: Cursor * fix: resolve skill validation errors - Move .template-versions.json from skills/ to root - Shorten skill names to meet 64-char limit: - salesforce-webapp-feature-micro-frontend-generating-micro-frontend-lwc → salesforce-webapp-micro-frontend-lwc - salesforce-webapp-feature-react-agentforce-conversation-client-integrating-agentforce-conversation-client → salesforce-webapp-agentforce-conversation-client - salesforce-webapp-feature-react-file-upload-implementing-file-upload → salesforce-webapp-react-file-upload - Expand descriptions to meet 20-word minimum with trigger context * Add webapp skills from template, sync script updates - Rename skill folders from salesforce-webapp-* to *-webapp-* convention - Update sync-template-skills.js: set SKILL.md front matter name to dest folder - Remove sync-template-skills workflow and pin-template-deps script - Add .synced-template-skills.json manifest, deploying-webapp-to-salesforce skill - Replace salesforce-webapp-designing-webapp-ui-ux with designing-webapp-ui-ux Made-with: Cursor * Align SKILL.md front matter name with folder for all webapp skills Made-with: Cursor * Fix skill validation: description length and trigger context for configuring-webapp-metadata, creating-webapp Made-with: Cursor * Rename sync script to sync-webapp-skills, drop manifest file - Rename sync-template-skills.js to sync-webapp-skills.js - Update package.json script to sync-webapp-skills - Remove .synced-template-skills.json creation and add to .gitignore Made-with: Cursor * Revert sync-react-b2e-sample and sync-react-b2x-sample to upstream version Made-with: Cursor * Sync script: pin b2e and b2x to latest, sync skills from template - Pin both template packages to latest in sync-webapp-skills.js - Update package.json / package-lock.json (b2x 1.109.0) - Sync skills: managing-webapp-agentforce-conversation-client, bar-line-chart, remove building-webapp-analytics-charts and integrating-webapp-agentforce-conversation-client - Minor skill content updates Made-with: Cursor * Remove interactive map, weather widget, and Unsplash skills (no longer in template) Made-with: Cursor --------- Co-authored-by: Hemant Singh Bisht <hsinghbisht@salesforce.com>
This commit is contained in:
parent
518df2074f
commit
9ba064174a
3
.gitignore
vendored
3
.gitignore
vendored
@ -219,6 +219,9 @@ target/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Sync script manifest (not created by script)
|
||||
skills/.synced-template-skills.json
|
||||
|
||||
# Local configuration
|
||||
config.local.*
|
||||
*.local.json
|
||||
|
||||
10
.template-versions.json
Normal file
10
.template-versions.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-base-sfdx-project-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-feature-react-file-upload-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-feature-react-chart-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental": "1.107.0",
|
||||
"@salesforce/webapp-template-feature-micro-frontend": "1.107.0",
|
||||
"@salesforce/webapps-features-experimental": "1.107.0"
|
||||
}
|
||||
48
package-lock.json
generated
48
package-lock.json
generated
@ -9,8 +9,8 @@
|
||||
"version": "1.1.0",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"devDependencies": {
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "^1.107.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "^1.107.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.109.1",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.109.1",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
},
|
||||
@ -686,22 +686,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/sdk-core": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/sdk-core/-/sdk-core-1.107.0.tgz",
|
||||
"integrity": "sha512-OV4NSD5WaDFSFVuiTtuDSxVAG5Z9qMFuUpgYfhmzdkK1GRFJERpa8LNqRqzI7GjrkR7z41yFB7ed0lRzYL8BAQ==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/sdk-core/-/sdk-core-1.109.1.tgz",
|
||||
"integrity": "sha512-czBnRRuNXfVN/3SADZs9xA+iBAA+l6W6ZrNFw7nw7D1dOfzFKoPI4oUtCa4ahe2ToSwpFlGD+iyngq2HaYCh2Q==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
"node_modules/@salesforce/sdk-data": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/sdk-data/-/sdk-data-1.107.0.tgz",
|
||||
"integrity": "sha512-C8J6lBKGL1gJBhFqkwoT9o1CxalhKDMkgkCt4UqV/hmd07DLdaXIxoUmtolPdkClv0f8LjijLOlUFXk6SvYJ3w==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/sdk-data/-/sdk-data-1.109.1.tgz",
|
||||
"integrity": "sha512-ep66OvR4Azi1+4as5+FeZFQB2ism1tX1bp6WQAElAJOu1UTqstsUFDGgdHaoXa2BwkER3kzuB/RWQzG7BueXsg==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"dependencies": {
|
||||
"@conduit-client/service-fetch-network": "3.17.0",
|
||||
"@conduit-client/utils": "3.17.0",
|
||||
"@salesforce/sdk-core": "^1.107.0"
|
||||
"@salesforce/sdk-core": "^1.109.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/ts-types": {
|
||||
@ -715,14 +715,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/webapp-experimental": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-experimental/-/webapp-experimental-1.107.0.tgz",
|
||||
"integrity": "sha512-7TTUTyDQ2kjF7gIVNmcIDF1ZbDPWR1xRIMIpaxoENJPpjMMTFy1+SOsRm1KF28/Lh6wP1FbuI5qXvcwM5zZyNA==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-experimental/-/webapp-experimental-1.109.1.tgz",
|
||||
"integrity": "sha512-x9o1xvep+zXOQZeGNo13M106yfXWWsYMfrBS8nGuH8NcWFQlss23vnvflVZnCKeG6Je/ZCZp/sHiOdSY4KGEhw==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"dependencies": {
|
||||
"@salesforce/core": "^8.23.4",
|
||||
"@salesforce/sdk-data": "^1.107.0",
|
||||
"@salesforce/sdk-data": "^1.109.1",
|
||||
"axios": "^1.7.7",
|
||||
"micromatch": "^4.0.8",
|
||||
"path-to-regexp": "^8.3.0"
|
||||
@ -732,27 +732,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/webapp-template-app-react-sample-b2e-experimental": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2e-experimental/-/webapp-template-app-react-sample-b2e-experimental-1.107.0.tgz",
|
||||
"integrity": "sha512-G34763Qan/uJx1rDeVi2ss3+KQSzA7ybypUpwog7gKFQ/EmtC//wkbo2/E4YzSFGhm4nZqjGeEADaTa+R3pZOA==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2e-experimental/-/webapp-template-app-react-sample-b2e-experimental-1.109.1.tgz",
|
||||
"integrity": "sha512-ju77WQp0dsIDroito/xgZrLI7DuIQfo8NgMLB8qVgnhE7Tssma8KAtqDHjK9HsmL/HTPlMXZUEhEqh8nJNq5Sw==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"dependencies": {
|
||||
"@salesforce/webapp-experimental": "^1.107.0",
|
||||
"@salesforce/webapp-template-feature-react-global-search-experimental": "^1.107.0"
|
||||
"@salesforce/webapp-experimental": "^1.109.1",
|
||||
"@salesforce/webapp-template-feature-react-global-search-experimental": "^1.109.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/webapp-template-app-react-sample-b2x-experimental": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2x-experimental/-/webapp-template-app-react-sample-b2x-experimental-1.107.0.tgz",
|
||||
"integrity": "sha512-ZNwqUI/l8llEtLR/gDiZ5Ijci5wJvnF72GxzddBKiyk3sIzTPMSXT4JsLpX4mHrOi/mvTKV20kE2qR/0H/4l2A==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2x-experimental/-/webapp-template-app-react-sample-b2x-experimental-1.109.1.tgz",
|
||||
"integrity": "sha512-Pf4cC1fbDJ5o7gJlztDCArqimQN60+iCAxNpHHDsz8hHUBO2lteD5jK3I9WSPeoBDNm7VMOgLFIssWiGPvRlVg==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
"node_modules/@salesforce/webapp-template-feature-react-global-search-experimental": {
|
||||
"version": "1.107.0",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-feature-react-global-search-experimental/-/webapp-template-feature-react-global-search-experimental-1.107.0.tgz",
|
||||
"integrity": "sha512-Kxo2+6hzie3JiDi/DPRqzQ4i2A6obYy1fJJS3mBn+P5EuAl3X8qH9yAY/B/haO6HU6PQHbvsqm/rnLkh3AJHXQ==",
|
||||
"version": "1.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@salesforce/webapp-template-feature-react-global-search-experimental/-/webapp-template-feature-react-global-search-experimental-1.109.1.tgz",
|
||||
"integrity": "sha512-w7qVrI3d7oMQKtUG982wgQpz+ZmlynxbYKIC3cJSnNVAAelOlRp77X1sq4tqu+nJ/HRsH7mx9blGXw6L903kvw==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
|
||||
@ -11,13 +11,14 @@
|
||||
"registry": "https://registry.npmjs.org"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "^1.107.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "^1.107.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.109.1",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.109.1",
|
||||
"tsx": "^4.21.0"
|
||||
},
|
||||
"scripts": {
|
||||
"validate:skills": "tsx scripts/validate-skills.ts",
|
||||
"sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js",
|
||||
"sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js"
|
||||
"sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js",
|
||||
"sync-webapp-skills": "node scripts/sync-webapp-skills.js"
|
||||
}
|
||||
}
|
||||
|
||||
118
scripts/sync-webapp-skills.js
Normal file
118
scripts/sync-webapp-skills.js
Normal file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Sync webapp skills: pins b2e and b2x template packages to latest npm versions,
|
||||
* runs npm install, then copies skills from dist/.a4drules/skills/ into skills/.
|
||||
* Run from repo root.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { copyRecursive } = require('./lib/copy-recursive');
|
||||
|
||||
const TEMPLATE_PACKAGES = [
|
||||
'@salesforce/webapp-template-app-react-sample-b2e-experimental',
|
||||
'@salesforce/webapp-template-app-react-sample-b2x-experimental',
|
||||
];
|
||||
const PACKAGE_NAME = TEMPLATE_PACKAGES[0]; // used for syncing skills
|
||||
const SKILLS_SRC = 'dist/.a4drules/skills';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pkgPath = path.join(repoRoot, 'package.json');
|
||||
const skillsDir = path.join(repoRoot, 'skills');
|
||||
|
||||
// ── Pin template packages to latest npm versions ────────────────────
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
let pkgChanged = false;
|
||||
for (const name of TEMPLATE_PACKAGES) {
|
||||
const current = (pkg.devDependencies || {})[name];
|
||||
if (!current || current.startsWith('file:')) continue;
|
||||
let latest;
|
||||
try {
|
||||
latest = execSync(`npm view ${name} version`, { encoding: 'utf8' }).trim();
|
||||
} catch (_) {
|
||||
console.warn(`Could not resolve ${name} on npm, using current version.`);
|
||||
continue;
|
||||
}
|
||||
if (current !== latest) {
|
||||
console.log(`${name}: ${current} -> ${latest}`);
|
||||
pkg.devDependencies[name] = latest;
|
||||
pkgChanged = true;
|
||||
}
|
||||
}
|
||||
if (pkgChanged) {
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
// ── Install ──────────────────────────────────────────────────────────
|
||||
console.log('Installing dependencies...');
|
||||
execSync('npm install', { cwd: repoRoot, stdio: 'inherit' });
|
||||
|
||||
// ── Sync skills ──────────────────────────────────────────────────────
|
||||
const pkgRoot = path.join(repoRoot, 'node_modules', PACKAGE_NAME.replace('/', path.sep));
|
||||
if (!fs.existsSync(pkgRoot)) {
|
||||
console.error(`Package not found at ${pkgRoot}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const srcDir = path.join(pkgRoot, SKILLS_SRC);
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.error(`Skills not found at ${srcDir}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
||||
|
||||
function addWebappPrefix(name) {
|
||||
const parts = name.split('-');
|
||||
if (parts.length < 2) return name;
|
||||
if (parts[1] === 'webapp') return name;
|
||||
return parts[0] + '-webapp-' + parts.slice(1).join('-');
|
||||
}
|
||||
|
||||
/** Dirs in skills/ that look like synced webapp skills (e.g. *-webapp-*, creating-webapp). */
|
||||
function isSyncedWebappSkillDir(name) {
|
||||
return name.includes('webapp');
|
||||
}
|
||||
|
||||
/** Set front matter `name` in SKILL.md to match the destination folder name. */
|
||||
function setSkillFrontMatterName(skillDir, destName) {
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
if (!fs.existsSync(skillPath)) return;
|
||||
let content = fs.readFileSync(skillPath, 'utf8');
|
||||
content = content.replace(/^name:\s*.+$/m, `name: ${destName}`);
|
||||
fs.writeFileSync(skillPath, content, 'utf8');
|
||||
}
|
||||
|
||||
// ── Clean up: remove skills no longer in the package ───────────────────
|
||||
const srcNames = fs.readdirSync(srcDir).filter((name) =>
|
||||
fs.statSync(path.join(srcDir, name)).isDirectory()
|
||||
);
|
||||
const currentDestNames = new Set(srcNames.map(addWebappPrefix));
|
||||
|
||||
for (const name of fs.readdirSync(skillsDir)) {
|
||||
const dirPath = path.join(skillsDir, name);
|
||||
if (!fs.statSync(dirPath).isDirectory()) continue;
|
||||
if (!isSyncedWebappSkillDir(name)) continue;
|
||||
if (currentDestNames.has(name)) continue;
|
||||
fs.rmSync(dirPath, { recursive: true });
|
||||
console.log(`Removed skills/${name}/ (no longer in package)`);
|
||||
}
|
||||
|
||||
// ── Copy each skill from package ──────────────────────────────────────
|
||||
const syncedDirs = [];
|
||||
for (const srcName of srcNames) {
|
||||
const src = path.join(srcDir, srcName);
|
||||
const destName = addWebappPrefix(srcName);
|
||||
const dest = path.join(skillsDir, destName);
|
||||
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
|
||||
copyRecursive(src, dest);
|
||||
setSkillFrontMatterName(dest, destName);
|
||||
syncedDirs.push(destName);
|
||||
console.log(`Synced skills/${destName}/`);
|
||||
}
|
||||
|
||||
const version = JSON.parse(
|
||||
fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')
|
||||
).version;
|
||||
console.log(`Done — synced ${syncedDirs.length} skills from ${PACKAGE_NAME}@${version}.`);
|
||||
178
skills/accessing-webapp-data/SKILL.md
Normal file
178
skills/accessing-webapp-data/SKILL.md
Normal file
@ -0,0 +1,178 @@
|
||||
---
|
||||
name: accessing-webapp-data
|
||||
description: Salesforce data access patterns. Use when adding or modifying any code that fetches data from Salesforce (records, Chatter, Connect API, etc.).
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce Data Access
|
||||
|
||||
Guidance for accessing Salesforce data from web apps. **All Salesforce data fetches MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK provides authentication, CSRF handling, and correct base URL resolution — direct `fetch` or `axios` calls bypass these and are not allowed.
|
||||
|
||||
## Mandatory: Use the Data SDK
|
||||
|
||||
> **Every Salesforce data fetch must go through the Data SDK.** Obtain it via `createDataSDK()`, then use `sdk.graphql?.()` or `sdk.fetch?.()`. Never call `fetch()` or `axios` directly for Salesforce endpoints.
|
||||
|
||||
## Optional Chaining and Graceful Handling
|
||||
|
||||
**Always use optional chaining** when calling `sdk.graphql` or `sdk.fetch` — these methods may be undefined in some surfaces (e.g., Salesforce ACC, MCP Apps). Handle the case where they are not available gracefully:
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
|
||||
// ✅ Use optional chaining
|
||||
const response = await sdk.graphql?.(query);
|
||||
|
||||
// ✅ Check before using fetch
|
||||
if (!sdk.fetch) {
|
||||
throw new Error("Data SDK fetch is not available in this context");
|
||||
}
|
||||
const res = await sdk.fetch(url);
|
||||
```
|
||||
|
||||
For GraphQL, if `sdk.graphql` is undefined, the call returns `undefined` — handle that in your logic (e.g., throw a clear error or return a fallback). For `sdk.fetch`, check availability before calling when the operation is required.
|
||||
|
||||
## Preference: GraphQL First
|
||||
|
||||
**GraphQL is the preferred method** for querying and mutating Salesforce records. Use it when:
|
||||
|
||||
- Querying records (Account, Contact, Opportunity, custom objects)
|
||||
- Creating, updating, or deleting records (when GraphQL supports the operation)
|
||||
- Fetching related data, filters, sorting, pagination
|
||||
|
||||
**Use `sdk.fetch` only when GraphQL is not sufficient.** For REST API usage, invoke the `fetching-rest-api` skill, which documents:
|
||||
|
||||
- Chatter API (e.g., `/services/data/v65.0/chatter/users/me`)
|
||||
- Connect REST API (e.g., `/services/data/v65.0/connect/file/upload/config`)
|
||||
- Apex REST (e.g., `/services/apexrest/auth/login`)
|
||||
- UI API REST (e.g., `/services/data/v65.0/ui-api/records/{recordId}`)
|
||||
- Einstein LLM Gateway
|
||||
|
||||
---
|
||||
|
||||
## Getting the SDK
|
||||
|
||||
```typescript
|
||||
import { createDataSDK } from "@salesforce/sdk-data";
|
||||
|
||||
const sdk = await createDataSDK();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 1: GraphQL (Preferred)
|
||||
|
||||
For record queries and mutations, use GraphQL via the Data SDK. Invoke the `using-graphql` skill for the full workflow (schema exploration, query authoring, codegen, lint validate).
|
||||
|
||||
```typescript
|
||||
import { createDataSDK, gql } from "@salesforce/sdk-data";
|
||||
import type { GetAccountsQuery } from "../graphql-operations-types";
|
||||
|
||||
const GET_ACCOUNTS = gql`
|
||||
query GetAccounts {
|
||||
uiapi {
|
||||
query {
|
||||
Account(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
Name { value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export async function getAccounts() {
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.graphql?.<GetAccountsQuery>(GET_ACCOUNTS);
|
||||
|
||||
if (response?.errors?.length) {
|
||||
throw new Error(response.errors.map((e) => e.message).join("; "));
|
||||
}
|
||||
|
||||
return response?.data?.uiapi?.query?.Account?.edges?.map((e) => e?.node) ?? [];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Fetch (When GraphQL Is Not Sufficient)
|
||||
|
||||
For REST endpoints that have no GraphQL equivalent, use `sdk.fetch`. **Invoke the `fetching-rest-api` skill** for full documentation of Chatter, Connect REST, Apex REST, UI API REST, and Einstein LLM endpoints.
|
||||
|
||||
```typescript
|
||||
import { createDataSDK } from "@salesforce/sdk-data";
|
||||
|
||||
declare const __SF_API_VERSION__: string;
|
||||
const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0";
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/chatter/users/me`);
|
||||
|
||||
if (!response?.ok) throw new Error(`HTTP ${response?.status}`);
|
||||
const data = await response.json();
|
||||
return { id: data.id, name: data.name };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Forbidden)
|
||||
|
||||
### Direct fetch to Salesforce
|
||||
|
||||
```typescript
|
||||
// ❌ FORBIDDEN — bypasses Data SDK auth and CSRF
|
||||
const res = await fetch("/services/data/v65.0/chatter/users/me");
|
||||
```
|
||||
|
||||
### Direct axios to Salesforce
|
||||
|
||||
```typescript
|
||||
// ❌ FORBIDDEN — bypasses Data SDK
|
||||
const res = await axios.get("/services/data/v65.0/chatter/users/me");
|
||||
```
|
||||
|
||||
### Correct approach
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT — use Data SDK
|
||||
const sdk = await createDataSDK();
|
||||
const res = await sdk.fetch?.("/services/data/v65.0/chatter/users/me");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Clarifying Vague Data Requests
|
||||
|
||||
When a user asks about data and the request is vague, **clarify before implementing**. Ask which of the following they want:
|
||||
|
||||
- **Application code** — Add or modify code in a specific web app so the app performs the data interaction at runtime (e.g., GraphQL query in the React app)
|
||||
- **Local SF CLI** — Run Salesforce CLI commands locally (e.g., `sf data query`, `sf data import tree`) to interact with the org from the terminal
|
||||
- **Local example data** — Update or add local fixture/example data files (e.g., JSON in `data/`) for development or testing
|
||||
- **Other** — Data export, report generation, setup script, etc.
|
||||
|
||||
Do not assume. A request like "fetch accounts" could mean: (1) add a GraphQL query to the app, (2) run `sf data query` in the terminal, or (3) update sample data files. Confirm the intent before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Decision Flow
|
||||
|
||||
1. **Need to query or mutate Salesforce records?** → Use GraphQL via the Data SDK. Invoke the `using-graphql` skill.
|
||||
2. **Need Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM?** → Use `sdk.fetch`. Invoke the `fetching-rest-api` skill.
|
||||
3. **Never** use `fetch`, `axios`, or similar directly for Salesforce API calls.
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- GraphQL workflow: invoke the `using-graphql` skill (`.a4drules/skills/using-graphql/`)
|
||||
- REST API via fetch: invoke the `fetching-rest-api` skill (`.a4drules/skills/fetching-rest-api/`)
|
||||
- Data SDK package: `@salesforce/sdk-data` (`createDataSDK`, `gql`, `NodeOfConnection`)
|
||||
- `createRecord` for UI API record creation: `@salesforce/webapp-experimental/api` (uses Data SDK internally)
|
||||
72
skills/building-webapp-data-visualization/SKILL.md
Normal file
72
skills/building-webapp-data-visualization/SKILL.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
name: building-webapp-data-visualization
|
||||
description: Adds data visualization components (charts, stat cards, KPI metrics) to React pages using Recharts. Use when the user asks to add a chart, graph, donut chart, pie chart, bar chart, stat card, KPI metric, dashboard visualization, or analytics component to the web application.
|
||||
---
|
||||
|
||||
# Data Visualization
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
- Adding charts (donut, pie, bar, line, area) to a dashboard or analytics page
|
||||
- Displaying KPI/metric stat cards with trend indicators
|
||||
- Building a dashboard layout with mixed chart types and summary cards
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Determine the visualization type
|
||||
|
||||
Identify what the user needs:
|
||||
|
||||
- **Donut / pie chart** — categorical breakdown (e.g. issue types, status distribution)
|
||||
- **Bar chart** — comparison across categories or time periods
|
||||
- **Line / area chart** — trends over time
|
||||
- **Stat card** — single KPI metric with optional trend indicator
|
||||
- **Combined dashboard** — stat cards + one or more charts
|
||||
|
||||
If unclear, ask:
|
||||
|
||||
> "What data should the chart display, and would a donut chart, bar chart, line chart, or stat cards work best?"
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Install dependencies
|
||||
|
||||
All chart types in this skill use **recharts**. Install once from the web app directory:
|
||||
|
||||
```bash
|
||||
npm install recharts
|
||||
```
|
||||
|
||||
Recharts is built on D3 and provides declarative React components. No additional CSS is needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Choose implementation path
|
||||
|
||||
Read the corresponding guide:
|
||||
|
||||
- **Bar chart** — read `implementation/bar-line-chart.md` (categorical data)
|
||||
- **Line / area chart** — read `implementation/bar-line-chart.md` (time-series data)
|
||||
- **Donut / pie chart** — read `implementation/donut-chart.md`
|
||||
- **Stat card with trend** — read `implementation/stat-card.md`
|
||||
- **Dashboard layout** — read `implementation/dashboard-layout.md`
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Before completing:
|
||||
|
||||
1. Chart renders with correct data and colors.
|
||||
2. Chart is responsive (resizes with container).
|
||||
3. Legend labels match the data categories.
|
||||
4. Stat card trends display correct positive/negative indicators.
|
||||
5. Run from the web app directory:
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
||||
```
|
||||
|
||||
- **Lint:** MUST result in 0 errors.
|
||||
- **Build:** MUST succeed.
|
||||
@ -0,0 +1,316 @@
|
||||
# Bar & Line / Area Chart — Implementation Guide
|
||||
|
||||
Requires **recharts** (install from the web app directory; see SKILL.md Step 2).
|
||||
|
||||
---
|
||||
|
||||
## Data shapes
|
||||
|
||||
### Time-series (line / area chart)
|
||||
|
||||
Use when data represents a trend over time or ordered sequence.
|
||||
|
||||
```ts
|
||||
interface TimeSeriesDataPoint {
|
||||
x: string; // date or label on the x-axis
|
||||
y: number; // numeric value
|
||||
}
|
||||
```
|
||||
|
||||
Map raw fields to this shape: e.g. `date` → `x`, `revenue` → `y`.
|
||||
|
||||
### Categorical (bar chart)
|
||||
|
||||
Use when data compares discrete categories.
|
||||
|
||||
```ts
|
||||
interface CategoricalDataPoint {
|
||||
name: string; // category label
|
||||
value: number; // numeric value
|
||||
}
|
||||
```
|
||||
|
||||
Map raw fields to this shape: e.g. `product` → `name`, `sales` → `value`.
|
||||
|
||||
### How to decide
|
||||
|
||||
| Signal | Type |
|
||||
|--------|------|
|
||||
| "over time", "trend", date-like keys | Time-series → line chart |
|
||||
| "by category", "by X", label-like keys | Categorical → bar chart |
|
||||
|
||||
---
|
||||
|
||||
## Theme colors
|
||||
|
||||
Pick a theme based on the data's sentiment:
|
||||
|
||||
| Theme | Stroke / Fill | When to use |
|
||||
|-------|---------------|-------------|
|
||||
| `green` | `#22c55e` | Growth, gain, positive trend |
|
||||
| `red` | `#ef4444` | Decline, loss, negative trend |
|
||||
| `neutral` | `#6366f1` | Default or mixed data |
|
||||
|
||||
Define colors as constants — do not use inline hex values.
|
||||
|
||||
```ts
|
||||
const THEME_COLORS = {
|
||||
red: "#ef4444",
|
||||
green: "#22c55e",
|
||||
neutral: "#6366f1",
|
||||
} as const;
|
||||
|
||||
type ChartTheme = keyof typeof THEME_COLORS;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Line chart component
|
||||
|
||||
Create at `components/LineChart.tsx` (or colocate with the page):
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import {
|
||||
LineChart as RechartsLineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
const THEME_COLORS = {
|
||||
red: "#ef4444",
|
||||
green: "#22c55e",
|
||||
neutral: "#6366f1",
|
||||
} as const;
|
||||
|
||||
type ChartTheme = keyof typeof THEME_COLORS;
|
||||
|
||||
interface TimeSeriesDataPoint {
|
||||
x: string;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface TimeSeriesChartProps {
|
||||
data: TimeSeriesDataPoint[];
|
||||
theme?: ChartTheme;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimeSeriesChart({
|
||||
data,
|
||||
theme = "neutral",
|
||||
title,
|
||||
className = "",
|
||||
}: TimeSeriesChartProps) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-muted-foreground text-center py-8">No data to display</p>;
|
||||
}
|
||||
|
||||
const color = THEME_COLORS[theme];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{title && (
|
||||
<h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RechartsLineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="x" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="y" stroke={color} strokeWidth={2} dot={false} />
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bar chart component
|
||||
|
||||
Create at `components/BarChart.tsx` (or colocate with the page):
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import {
|
||||
BarChart as RechartsBarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
const THEME_COLORS = {
|
||||
red: "#ef4444",
|
||||
green: "#22c55e",
|
||||
neutral: "#6366f1",
|
||||
} as const;
|
||||
|
||||
type ChartTheme = keyof typeof THEME_COLORS;
|
||||
|
||||
interface CategoricalDataPoint {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface CategoricalChartProps {
|
||||
data: CategoricalDataPoint[];
|
||||
theme?: ChartTheme;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CategoricalChart({
|
||||
data,
|
||||
theme = "neutral",
|
||||
title,
|
||||
className = "",
|
||||
}: CategoricalChartProps) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-muted-foreground text-center py-8">No data to display</p>;
|
||||
}
|
||||
|
||||
const color = THEME_COLORS[theme];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{title && (
|
||||
<h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RechartsBarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Area chart variant
|
||||
|
||||
For a filled area chart (useful for volume-over-time), swap `Line` for `Area`:
|
||||
|
||||
```tsx
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
||||
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="x" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="y" stroke={color} fill={color} fillOpacity={0.2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chart container wrapper
|
||||
|
||||
Wrap any chart in a styled card for consistent spacing:
|
||||
|
||||
```tsx
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface ChartContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ChartContainer({ children, className = "" }: ChartContainerProps) {
|
||||
return (
|
||||
<Card className={`p-4 border-gray-200 shadow-sm ${className}`}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```tsx
|
||||
<ChartContainer>
|
||||
<TimeSeriesChart data={monthlyData} theme="green" title="Monthly Revenue" />
|
||||
</ChartContainer>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Preparing raw data
|
||||
|
||||
Map API responses to the expected shape before passing to the chart:
|
||||
|
||||
```tsx
|
||||
const timeSeriesData = useMemo(
|
||||
() => apiRecords.map((r) => ({ x: r.date, y: r.revenue })),
|
||||
[apiRecords],
|
||||
);
|
||||
|
||||
const categoricalData = useMemo(
|
||||
() => apiRecords.map((r) => ({ name: r.product, value: r.sales })),
|
||||
[apiRecords],
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Recharts concepts
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ResponsiveContainer` | Wraps chart to fill parent width |
|
||||
| `CartesianGrid` | Background grid lines |
|
||||
| `XAxis` / `YAxis` | Axis labels; `dataKey` maps to the data field |
|
||||
| `Tooltip` | Hover info |
|
||||
| `Legend` | Series labels |
|
||||
| `Line` | Line series; `type="monotone"` for smooth curves |
|
||||
| `Bar` | Bar series; `radius` rounds top corners |
|
||||
| `Area` | Filled area; `fillOpacity` controls transparency |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Always include a text legend (not just colors).
|
||||
- Chart should be wrapped in a section with a visible heading.
|
||||
- For critical data, provide a text summary or table alternative.
|
||||
- Use sufficient color contrast between the chart stroke/fill and background.
|
||||
- Consider `prefers-reduced-motion` for chart animations.
|
||||
|
||||
---
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Missing `ResponsiveContainer` | Chart won't resize; always wrap |
|
||||
| Fixed width/height on chart | Let `ResponsiveContainer` control sizing |
|
||||
| No empty-data handling | Show "No data" message when `data.length === 0` |
|
||||
| Inline colors | Extract to `THEME_COLORS` constant |
|
||||
| Using raw Recharts for every chart type | Use `DonutChart` (see `donut-chart.md`) for pie/donut |
|
||||
@ -0,0 +1,189 @@
|
||||
# Dashboard Layout — Implementation Guide
|
||||
|
||||
## Anatomy of a dashboard page
|
||||
|
||||
A typical dashboard combines stat cards, charts, and data tables:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Search / global action bar │
|
||||
├──────────┬──────────┬──────────────────────────────┤
|
||||
│ Stat 1 │ Stat 2 │ Stat 3 │
|
||||
├──────────┴──────────┴──────┬───────────────────────┤
|
||||
│ │ │
|
||||
│ Data table / list │ Donut chart │
|
||||
│ (70% width) │ (30% width) │
|
||||
│ │ │
|
||||
└────────────────────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout implementation
|
||||
|
||||
```tsx
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
import { DonutChart } from "@/components/DonutChart";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Search bar */}
|
||||
<div>{/* global search component */}</div>
|
||||
|
||||
{/* Main content: 70/30 split */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
||||
<div className="space-y-6">
|
||||
{/* Stat cards row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<StatCard title="Metric A" value={42} />
|
||||
<StatCard title="Metric B" value={18} />
|
||||
<StatCard title="Metric C" value={7} />
|
||||
</div>
|
||||
|
||||
{/* Data table */}
|
||||
<div>{/* table component */}</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar chart */}
|
||||
<div>
|
||||
<DonutChart title="Distribution" data={chartData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive behavior
|
||||
|
||||
| Breakpoint | Layout |
|
||||
|------------|--------|
|
||||
| Mobile (`< 768px`) | Single column, everything stacked |
|
||||
| Tablet (`md`) | Stat cards in 3-col grid, rest stacked |
|
||||
| Desktop (`lg`) | 70/30 split for table + chart |
|
||||
|
||||
Key Tailwind classes:
|
||||
|
||||
```
|
||||
grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6
|
||||
grid grid-cols-1 md:grid-cols-3 gap-6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading state
|
||||
|
||||
Show a full-page loading state while dashboard data is being fetched:
|
||||
|
||||
```tsx
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">Loading dashboard…</p>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Or use a skeleton layout:
|
||||
|
||||
```tsx
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-28 animate-pulse rounded-xl bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
||||
<div className="h-64 animate-pulse rounded-xl bg-muted" />
|
||||
<div className="h-64 animate-pulse rounded-xl bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data fetching pattern
|
||||
|
||||
Use `useEffect` with cancellation for dashboard metrics:
|
||||
|
||||
```ts
|
||||
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchDashboardMetrics();
|
||||
if (!cancelled) setMetrics(data);
|
||||
} catch (error) {
|
||||
if (!cancelled) console.error("Error loading metrics:", error);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Combining multiple data sources
|
||||
|
||||
Dashboards often aggregate data from several APIs. Load them in parallel:
|
||||
|
||||
```ts
|
||||
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
||||
const [requests, setRequests] = useState<Request[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([fetchMetrics(), fetchRecentRequests()])
|
||||
.then(([metricsData, requestsData]) => {
|
||||
if (!cancelled) {
|
||||
setMetrics(metricsData);
|
||||
setRequests(requestsData);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PageContainer wrapper
|
||||
|
||||
A simple wrapper for consistent page padding:
|
||||
|
||||
```tsx
|
||||
interface PageContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageContainer({ children }: PageContainerProps) {
|
||||
return <div className="p-6">{children}</div>;
|
||||
}
|
||||
```
|
||||
@ -0,0 +1,181 @@
|
||||
# Donut / Pie Chart — Implementation Guide
|
||||
|
||||
Requires **recharts** (install from the web app directory; see SKILL.md Step 2).
|
||||
|
||||
---
|
||||
|
||||
## Data structure
|
||||
|
||||
Charts expect an array of objects with `name`, `value`, and `color`:
|
||||
|
||||
```ts
|
||||
interface ChartData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Donut chart component
|
||||
|
||||
Create at `components/DonutChart.tsx`:
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface ChartData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface DonutChartProps {
|
||||
title: string;
|
||||
data: ChartData[];
|
||||
}
|
||||
|
||||
export const DonutChart: React.FC<DonutChartProps> = ({ title, data }) => {
|
||||
const total = data.reduce((sum, item) => sum + item.value, 0);
|
||||
const mainPercentage = total > 0 ? Math.round((data[0]?.value / total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<Card className="p-4 border-gray-200 shadow-sm flex flex-col">
|
||||
<h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<div className="relative flex items-center justify-center">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={70}
|
||||
outerRadius={110}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
{/* Center label */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl font-bold text-primary">{mainPercentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 grid grid-cols-2 gap-3">
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.color }} />
|
||||
<span className="text-sm text-gray-700">{item.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Recharts concepts
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ResponsiveContainer` | Wraps chart to make it fill its parent's width |
|
||||
| `PieChart` | Chart container for pie/donut |
|
||||
| `Pie` | The data ring; `innerRadius` > 0 makes it a donut |
|
||||
| `Cell` | Individual segment; accepts `fill` color |
|
||||
| `paddingAngle` | Gap between segments (degrees) |
|
||||
|
||||
### Donut vs Pie
|
||||
|
||||
| Property | Donut | Pie |
|
||||
|----------|-------|-----|
|
||||
| `innerRadius` | `> 0` (e.g. `70`) | `0` |
|
||||
| Center label | Yes, positioned absolutely | Not typical |
|
||||
|
||||
---
|
||||
|
||||
## Preparing chart data from raw records
|
||||
|
||||
Transform API data into the `ChartData[]` format before passing to the chart:
|
||||
|
||||
```tsx
|
||||
const CATEGORIES = ["Plumbing", "HVAC", "Electrical"] as const;
|
||||
const OTHER_LABEL = "Other";
|
||||
const COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"];
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
CATEGORIES.forEach((c) => (counts[c] = 0));
|
||||
counts[OTHER_LABEL] = 0;
|
||||
|
||||
records.forEach((record) => {
|
||||
const type = record.category;
|
||||
if (CATEGORIES.includes(type as (typeof CATEGORIES)[number])) {
|
||||
counts[type]++;
|
||||
} else {
|
||||
counts[OTHER_LABEL]++;
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
...CATEGORIES.map((name, i) => ({ name, value: counts[name], color: COLORS[i] })),
|
||||
{ name: OTHER_LABEL, value: counts[OTHER_LABEL], color: COLORS[CATEGORIES.length] },
|
||||
];
|
||||
}, [records]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Color palette recommendations
|
||||
|
||||
| Use case | Colors |
|
||||
|----------|--------|
|
||||
| Categorical (4 items) | `#7C3AED` `#EC4899` `#14B8A6` `#06B6D4` |
|
||||
| Status (3 items) | `#22C55E` `#F59E0B` `#EF4444` (green/amber/red) |
|
||||
| Sequential | Use opacity variants of one hue: `#7C3AED` at 100%, 75%, 50%, 25% |
|
||||
|
||||
Keep chart colors consistent with the app's design system. Define them as constants, not inline values.
|
||||
|
||||
---
|
||||
|
||||
## Other chart types
|
||||
|
||||
For **bar charts** and **line / area charts**, see `bar-line-chart.md` in this directory.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Always include a text legend (not just colors).
|
||||
- Chart should be wrapped in a section with a visible heading.
|
||||
- For critical data, provide a text summary or table alternative.
|
||||
- Use sufficient color contrast between segments.
|
||||
- Consider `prefers-reduced-motion` for chart animations.
|
||||
|
||||
---
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Missing `ResponsiveContainer` | Chart won't resize; always wrap in `ResponsiveContainer` |
|
||||
| Fixed width/height on `PieChart` | Let `ResponsiveContainer` control sizing |
|
||||
| No legend | Add a grid legend below the chart |
|
||||
| Inline colors | Extract to constants for consistency |
|
||||
| No fallback for empty data | Show "No data" message when `data` is empty |
|
||||
@ -0,0 +1,150 @@
|
||||
# Stat Card — Implementation Guide
|
||||
|
||||
## What is a stat card
|
||||
|
||||
A stat card displays a single KPI metric with an optional trend indicator. Used on dashboards to show at-a-glance numbers like "Total Properties: 42 (+10%)".
|
||||
|
||||
---
|
||||
|
||||
## Component interface
|
||||
|
||||
```ts
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: number | string;
|
||||
trend?: {
|
||||
value: number;
|
||||
isPositive: boolean;
|
||||
};
|
||||
subtitle?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## StatCard component
|
||||
|
||||
Create at `components/StatCard.tsx`:
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: number | string;
|
||||
trend?: {
|
||||
value: number;
|
||||
isPositive: boolean;
|
||||
};
|
||||
subtitle?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const StatCard: React.FC<StatCardProps> = ({ title, value, trend, subtitle, onClick }) => {
|
||||
return (
|
||||
<Card
|
||||
className={`p-4 border-gray-200 shadow-sm relative ${
|
||||
onClick ? "cursor-pointer hover:shadow-lg transition-shadow" : ""
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground uppercase tracking-wide">{title}</p>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<p className="text-4xl font-bold text-primary">{value}</p>
|
||||
{trend && (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-sm font-medium ${
|
||||
trend.isPositive
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-pink-100 text-pink-800"
|
||||
}`}
|
||||
>
|
||||
{trend.isPositive ? (
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
) : (
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
)}
|
||||
{Math.abs(trend.value)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && <p className="text-sm text-muted-foreground mt-1">{subtitle}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
This version uses Lucide icons (`TrendingUp`/`TrendingDown`) instead of custom SVGs for portability across projects.
|
||||
|
||||
---
|
||||
|
||||
## Layout: stat card grid
|
||||
|
||||
Display stat cards in a responsive grid:
|
||||
|
||||
```tsx
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<StatCard
|
||||
title="Total Properties"
|
||||
value={metrics.totalProperties}
|
||||
trend={{ value: 10, isPositive: true }}
|
||||
subtitle="Last month total 38"
|
||||
/>
|
||||
<StatCard
|
||||
title="Units Available"
|
||||
value={metrics.unitsAvailable}
|
||||
trend={{ value: 5, isPositive: false }}
|
||||
subtitle="Last month total 12/42"
|
||||
/>
|
||||
<StatCard
|
||||
title="Occupied Units"
|
||||
value={metrics.occupiedUnits}
|
||||
trend={{ value: 8, isPositive: true }}
|
||||
subtitle="Last month total 27"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Computing trend values
|
||||
|
||||
Calculate trends from current vs previous period:
|
||||
|
||||
```ts
|
||||
const trends = useMemo(() => {
|
||||
const previousTotal = metrics.totalProperties - Math.round(metrics.totalProperties * 0.1);
|
||||
const trendPercent = previousTotal > 0
|
||||
? Math.round(((metrics.totalProperties - previousTotal) / previousTotal) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
value: Math.abs(trendPercent),
|
||||
isPositive: trendPercent >= 0,
|
||||
};
|
||||
}, [metrics]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trend badge color conventions
|
||||
|
||||
| Trend | Background | Text | Meaning |
|
||||
|-------|------------|------|---------|
|
||||
| Positive (up) | `bg-emerald-100` | `text-emerald-800` | Growth, improvement |
|
||||
| Negative (down) | `bg-pink-100` | `text-pink-800` | Decline, concern |
|
||||
| Neutral | `bg-gray-100` | `text-gray-600` | No change |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Card uses `cursor-pointer` and `hover:shadow-lg` only when `onClick` is provided.
|
||||
- Trend icons have implicit meaning from color + direction icon.
|
||||
- Stat values use large, bold text for visibility.
|
||||
- Title uses `uppercase tracking-wide` for visual hierarchy without heading tags (appropriate in a card grid).
|
||||
96
skills/building-webapp-react-components/SKILL.md
Normal file
96
skills/building-webapp-react-components/SKILL.md
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
name: building-webapp-react-components
|
||||
description: Use when editing any React code in the web application — creating or modifying components, pages, layout, headers, footers, or any TSX/JSX files. Follow this skill for add component, add page, header/footer, and general React UI implementation patterns (shadcn UI and Tailwind CSS).
|
||||
---
|
||||
|
||||
# React Web App (Components, Pages, Layout)
|
||||
|
||||
Use this skill whenever you are editing React/TSX code in the web app (creating or modifying components, pages, header/footer, or layout).
|
||||
|
||||
## Step 1 — Identify the type of component
|
||||
|
||||
Determine which of these three categories the request falls into, then follow the corresponding section below:
|
||||
|
||||
- **Page** — user wants a new routed page (e.g. "add a contacts page", "create a dashboard page", "add a settings section")
|
||||
- **Header / Footer** — user wants a site-wide header, footer, nav bar, or page footer that appears on every page
|
||||
- **Component** — everything else: a widget, card, table, form, dialog, or other UI element placed within an existing page
|
||||
|
||||
If it is not immediately clear from the user's message, ask:
|
||||
|
||||
> "Are you looking to add a new page, a site-wide header or footer, or a component within an existing page?"
|
||||
|
||||
Then follow the matching section.
|
||||
|
||||
---
|
||||
|
||||
## Clarifying Questions
|
||||
|
||||
Ask **one question at a time** and wait for the response before asking the next. Stop when you have enough to build accurately — do not guess or assume.
|
||||
|
||||
### For a Page
|
||||
|
||||
1. **What is the name and purpose of the page?** (e.g., Contacts, Dashboard, Settings)
|
||||
2. **What URL path should it use?** (e.g., `/contacts`, `/dashboard`) — or derive from the page name?
|
||||
3. **Should the page appear in the navigation menu?**
|
||||
4. **Who can access it?** Public, authenticated users only (`PrivateRoute`), or unauthenticated only (e.g., login — `AuthenticationRoute`)?
|
||||
5. **What content or sections should the page include?** (list, form, table, detail view, etc.)
|
||||
6. **Does it need to fetch any data?** If so, from where?
|
||||
|
||||
### For a Header / Footer
|
||||
|
||||
1. **Header, footer, or both?**
|
||||
2. **What should the header contain?** (logo/app name, nav links, user avatar, CTA button, etc.)
|
||||
3. **What should the footer contain?** (copyright text, links, social icons, etc.)
|
||||
4. **Should the header be sticky (fixed to top while scrolling)?**
|
||||
5. **Is there a logo or brand name to display?** (or placeholder?)
|
||||
6. **Any specific color scheme or style direction?** (dark background, branded primary color, minimal, etc.)
|
||||
7. **Should navigation links appear in the header?** If so, which pages?
|
||||
|
||||
### For a Component
|
||||
|
||||
1. **What should the component do?** (display data, accept input, trigger an action, etc.)
|
||||
2. **What page or location should it appear on?**
|
||||
3. **Is this shared/reusable across pages, or specific to one feature?** (determines file location)
|
||||
4. **What data or props does it need?** (static content, props, fetched data)
|
||||
5. **Does it need internal state?** (loading, toggle, form state, etc.)
|
||||
6. **Are there any specific shadcn components to use?** (Card, Table, Dialog, Form, etc.)
|
||||
7. **Should it appear in a specific layout position?** (full-width, sidebar, inline, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
Once you have identified the type and gathered answers to the clarifying questions, read and follow the corresponding implementation guide:
|
||||
|
||||
- **Page** — read `implementation/page.md` and follow the instructions there.
|
||||
- **Header / Footer** — read `implementation/header-footer.md` and follow the instructions there.
|
||||
- **Component** — read `implementation/component.md` and follow the instructions there.
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Standards
|
||||
|
||||
- **Never use `any`** — use proper types, generics, or `unknown` with type guards.
|
||||
- **Event handlers:** `(event: React.FormEvent<HTMLFormElement>): void`
|
||||
- **State:** `useState<User | null>(null)` — always provide the type parameter.
|
||||
- **No unsafe assertions** (`obj as User`). Use type guards:
|
||||
```typescript
|
||||
function isUser(obj: unknown): obj is User {
|
||||
return typeof obj === 'object' && obj !== null && typeof (obj as User).id === 'string';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification (MANDATORY)
|
||||
|
||||
Before completing, run from the web app directory `force-app/main/default/webapplications/<appName>/`:
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
||||
```
|
||||
|
||||
- **Lint:** MUST result in 0 errors.
|
||||
- **Build:** MUST succeed (includes TypeScript check).
|
||||
|
||||
If either fails, fix the errors and re-run. Do not leave the session with failing quality gates.
|
||||
@ -0,0 +1,78 @@
|
||||
# Implementation — Component
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Always use shadcn components** from `@/components/ui` — never build raw HTML equivalents for buttons, inputs, cards, alerts, tabs, tables, or labels.
|
||||
2. **All styling via Tailwind** — utility classes only. No inline `style={{}}`, CSS Modules, or other styling systems.
|
||||
3. **Use design tokens** — prefer `bg-background`, `text-foreground`, `text-muted-foreground`, `border`, `bg-primary`, `text-destructive`, `rounded-lg` over hardcoded colors.
|
||||
4. **Use `cn()`** from `@/lib/utils` for conditional or composable class names.
|
||||
5. **TypeScript** — functional components with typed props interface; always accept `className?: string`.
|
||||
|
||||
### File Location — Component
|
||||
|
||||
| Component type | Location | Export |
|
||||
| ---------------------------------------------- | ---------------------------------------- | ---------------------------------------- |
|
||||
| Shared UI primitive (reusable across features) | `src/components/ui/` — add to `index.ts` | Named export |
|
||||
| Feature-specific (e.g., dashboard widget) | `src/components/<feature>/` | Named export, import directly where used |
|
||||
| Page-level layout element | `src/components/layout/` | Named export |
|
||||
|
||||
### Component Structure
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MyComponentProps {
|
||||
title: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MyComponent({ title, value, className }: MyComponentProps) {
|
||||
return (
|
||||
<Card className={cn("border", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold text-foreground">{value}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### State and Hooks
|
||||
|
||||
- **Local state only:** keep `useState`, `useReducer`, `useRef` inside the component.
|
||||
- **Shared or complex state:** extract to a custom hook in `src/hooks/` (prefix with `use`, e.g. `useFormData`). Do this when more than one component needs the state, or when multiple hooks are composed together.
|
||||
|
||||
### Adding the Component to a Page
|
||||
|
||||
```tsx
|
||||
// In the target page file, e.g. src/pages/HomePage.tsx
|
||||
import { MyComponent } from "@/components/<feature>/MyComponent";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<MyComponent title="Status" value="Active" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Useful Patterns — Component
|
||||
|
||||
- **Programmatic navigation:** use `useNavigate` from `react-router`; call `navigate(path)` — consistent with GlobalSearchInput, SearchResultCard, MaintenanceTable, and other components in the web application.
|
||||
- **Page container:** `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12`
|
||||
- **Icons:** `lucide-react`; add `aria-hidden="true"` on decorative icons
|
||||
- **Focus styles:** use `focus-visible:` variants
|
||||
- **Multiple visual variants:** use CVA (`cva`) and `VariantProps`
|
||||
- **shadcn import barrel:** `import { Button, Card, Input } from "@/components/ui"`
|
||||
|
||||
### Confirm — Component
|
||||
|
||||
- Imports use path aliases (`@/`, not deep relative paths)
|
||||
- No raw `<button>`, `<input>`, or styled `<div>` where shadcn equivalents exist
|
||||
- No inline `style={{}}` — Tailwind only
|
||||
@ -0,0 +1,132 @@
|
||||
# Implementation — Header / Footer
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Edit `appLayout.tsx` only** — header and footer are layout-level concerns. Never add them to individual page files.
|
||||
2. **Never modify `routes.tsx` or `app.tsx`** — the router setup must remain intact.
|
||||
3. **Create component files in `src/components/layout/`** — the designated location for layout-level components.
|
||||
4. **Use the full-height flex column pattern** — wrap layout in `min-h-screen flex flex-col` so footer stays at bottom.
|
||||
5. **Use shadcn and Tailwind** — compose from `@/components/ui`; style with Tailwind utility classes and design tokens.
|
||||
6. **Use path aliases** — import with `@/components/layout/...` and `@/components/ui`; no deep relative paths.
|
||||
7. **Preserve existing content** — if `appLayout.tsx` already has a `<NavigationMenu />` or other shell elements, keep them in place.
|
||||
|
||||
### Step 1 — Create the header component (if requested)
|
||||
|
||||
Create `src/components/layout/AppHeader.tsx`:
|
||||
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AppHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AppHeader({ className }: AppHeaderProps) {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"w-full border-b bg-background px-4 sm:px-6 lg:px-8 py-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<span className="text-lg font-semibold text-foreground">My App</span>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — Create the footer component (if requested)
|
||||
|
||||
Create `src/components/layout/AppFooter.tsx`:
|
||||
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AppFooterProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AppFooter({ className }: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"w-full border-t bg-background px-4 sm:px-6 lg:px-8 py-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} My App. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Edit `appLayout.tsx`
|
||||
|
||||
Open `src/appLayout.tsx` — this is the **only file to modify** for layout-level additions. Wrap existing content in a flex column and add header above and footer below `<Outlet />`:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from "react-router";
|
||||
import { AppHeader } from "@/components/layout/AppHeader";
|
||||
import { AppFooter } from "@/components/layout/AppFooter";
|
||||
// Keep all existing imports unchanged
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<AppHeader />
|
||||
{/* Keep any existing NavigationMenu or other shell elements here */}
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### File Locations — Header / Footer
|
||||
|
||||
| Component | File | Export |
|
||||
| ------------ | ------------------------------------- | ------------------------------ |
|
||||
| Header | `src/components/layout/AppHeader.tsx` | Named export |
|
||||
| Footer | `src/components/layout/AppFooter.tsx` | Named export |
|
||||
| Layout shell | `src/appLayout.tsx` | Default export (edit in place) |
|
||||
|
||||
### Why `appLayout.tsx` — Not Pages or Routes
|
||||
|
||||
`AppLayout` is the single shell rendered at the root route. Every page is a child rendered via `<Outlet />`. Placing the header and footer here ensures they appear on every page without touching individual pages or the route registry.
|
||||
|
||||
```
|
||||
AppLayout (appLayout.tsx)
|
||||
├── AppHeader ← renders on every page
|
||||
├── NavigationMenu ← keep if already present
|
||||
├── <Outlet /> ← active page renders here
|
||||
└── AppFooter ← renders on every page
|
||||
```
|
||||
|
||||
### Useful Patterns — Header / Footer
|
||||
|
||||
- **Sticky header:** add `sticky top-0 z-50` to the `<header>` element
|
||||
- **Separator:** use `<Separator />` from `@/components/ui` instead of `border-b`/`border-t` if a visible divider is preferred
|
||||
- **Nav links in header:** use `<Button variant="ghost" asChild>` wrapping a React Router `<Link>`
|
||||
- **Icons:** `lucide-react`; add `aria-hidden="true"` on decorative icons
|
||||
- **Design tokens:** `bg-background`, `text-foreground`, `text-muted-foreground`, `border`, `bg-primary`
|
||||
|
||||
### Mobile hamburger / Menu icon — Must be functional
|
||||
|
||||
If the header includes a hamburger or `Menu` icon for mobile:
|
||||
|
||||
- **Do not** add a Menu/hamburger icon that does nothing. It must toggle a visible mobile menu.
|
||||
- **Required:** (1) State: `const [isOpen, setIsOpen] = useState(false)`. (2) Button: `onClick={() => setIsOpen(!isOpen)}`, `aria-label="Toggle menu"`. (3) Conditional panel: `{isOpen && ( <div>...nav links...</div> )}` with responsive visibility (e.g. `md:hidden`). (4) Close on navigate: each link in the panel should `onClick={() => setIsOpen(false)}`.
|
||||
- Implement in `appLayout.tsx` (or the component that owns the header). Use the `Menu` icon from `lucide-react`.
|
||||
|
||||
### Confirm — Header / Footer
|
||||
|
||||
- Header and footer appear on every page (navigate to at least two routes)
|
||||
- Imports use path aliases (`@/components/layout/...`)
|
||||
- No inline `style={{}}` — Tailwind only
|
||||
- `src/routes.tsx` and `src/app.tsx` are unchanged
|
||||
@ -0,0 +1,93 @@
|
||||
# Implementation — Page
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Edit the component that owns the UI, never output raw HTML** — When editing the home page or any page content, modify the actual `.tsx` file that renders the target. If the target is inside a child component (e.g. `<GlobalSearchInput />` in `Home.tsx`), edit the child's file (e.g. `GlobalSearchInput.tsx`), not the parent. Do not wrap the component with extra elements in the parent; go into the component and change its JSX. Do not paste or generate raw HTML.
|
||||
2. **`routes.tsx` is the only route registry** — never add routes in `app.tsx` or inside page files.
|
||||
3. **All pages are children of the AppLayout route** — do not create top-level routes that bypass the layout shell.
|
||||
4. **Default export per page** — each page file has exactly one default-export component.
|
||||
5. **Path aliases in all imports** — use `@/pages/...`, `@/components/...`; no deep relative paths.
|
||||
6. **No inline styles** — Tailwind utility classes and design tokens only.
|
||||
7. **Catch-all last** — `path: '*'` (NotFound) must always remain the last child in the layout route.
|
||||
8. **Never modify `appLayout.tsx`** when adding a page — layout changes are a separate concern.
|
||||
|
||||
### Step 1 — Create the page file
|
||||
|
||||
Create `src/pages/MyPage.tsx` with a **default export** and the standard page container:
|
||||
|
||||
```tsx
|
||||
export default function MyPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h1 className="text-3xl font-bold text-foreground">My Page</h1>
|
||||
<p className="mt-4 text-muted-foreground">Page content goes here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Use shadcn components from `@/components/ui` for UI elements. All styling via Tailwind — no inline `style={{}}`.
|
||||
|
||||
### Step 2 — Register the route in `routes.tsx`
|
||||
|
||||
Open `src/routes.tsx`. Import the page and add it inside the layout route's `children` array:
|
||||
|
||||
```tsx
|
||||
import MyPage from "@/pages/MyPage";
|
||||
|
||||
// Inside the layout route's children array (before the catch-all):
|
||||
{
|
||||
path: "my-page",
|
||||
element: <MyPage />,
|
||||
handle: { showInNavigation: true, label: "My Page" },
|
||||
},
|
||||
```
|
||||
|
||||
- `path` is a **relative segment** (e.g., `"contacts"`), not an absolute path.
|
||||
- Include `handle: { showInNavigation: true, label: "Label" }` only if the page should appear in the navigation menu.
|
||||
- The catch-all `path: '*'` must stay **last**.
|
||||
|
||||
### Step 3 — Apply an auth guard (if needed)
|
||||
|
||||
| Access type | Guard | Behavior |
|
||||
| ---------------------------------- | ----------------------- | --------------------------------------- |
|
||||
| Public | None | Direct child of layout |
|
||||
| Authenticated only | `<PrivateRoute>` | Redirects to login if not authenticated |
|
||||
| Unauthenticated only (e.g., login) | `<AuthenticationRoute>` | Redirects away if already authenticated |
|
||||
|
||||
Example — private page:
|
||||
|
||||
```tsx
|
||||
import { PrivateRoute } from "@/components/auth/private-route";
|
||||
|
||||
{
|
||||
path: "settings",
|
||||
element: <PrivateRoute><SettingsPage /></PrivateRoute>,
|
||||
handle: { showInNavigation: true, label: "Settings" },
|
||||
},
|
||||
```
|
||||
|
||||
Use `ROUTES.*` constants from `@/utils/authenticationConfig` for auth-related paths — do not hardcode `/login`, `/profile`, etc.
|
||||
|
||||
### File Conventions — Page
|
||||
|
||||
| Concern | Location |
|
||||
| ----------------- | ------------------------------------------------------ |
|
||||
| Page component | `src/pages/<PageName>.tsx` (default export) |
|
||||
| Route definition | `src/routes.tsx` only |
|
||||
| Layout shell | `src/appLayout.tsx` — do not modify for page additions |
|
||||
| Auth config paths | `ROUTES.*` from `@/utils/authenticationConfig` |
|
||||
|
||||
### State and Data
|
||||
|
||||
- **Local state:** `useState`, `useReducer`, `useRef` inside the page component
|
||||
- **Shared or complex state:** extract to `src/hooks/` with a `use` prefix (e.g., `useContacts`)
|
||||
- **Data fetching:** prefer GraphQL (`executeGraphQL`) or REST utilities in `src/api/`; place shared data logic in `src/hooks/`
|
||||
- **Auth context:** `useAuth()` from `@/context/AuthContext` when current user is needed — only valid under `AuthProvider`
|
||||
|
||||
### Confirm — Page
|
||||
|
||||
- The page renders inside the app shell (header/nav visible)
|
||||
- If `showInNavigation: true`, the link appears in the navigation menu
|
||||
- No TypeScript errors; no broken imports; no missing exports
|
||||
- Imports use path aliases (`@/`, not deep relative paths)
|
||||
90
skills/configuring-webapp-csp-trusted-sites/SKILL.md
Normal file
90
skills/configuring-webapp-csp-trusted-sites/SKILL.md
Normal file
@ -0,0 +1,90 @@
|
||||
---
|
||||
name: configuring-webapp-csp-trusted-sites
|
||||
description: Creates Salesforce CSP Trusted Site metadata when adding external domains. Use when the user adds an external API, CDN, image host, font provider, map tile server, or any third-party URL that the web application needs to load resources from — or when a browser console shows a CSP violation error.
|
||||
---
|
||||
|
||||
# CSP Trusted Sites
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill whenever the application references a new external domain that is not already registered as a CSP Trusted Site. This includes:
|
||||
|
||||
- Adding images from a new CDN (Unsplash, Pexels, Cloudinary, etc.)
|
||||
- Loading fonts from an external provider (Google Fonts, Adobe Fonts)
|
||||
- Calling a third-party API (Open-Meteo, Nominatim, Mapbox, etc.)
|
||||
- Loading map tiles from a tile server (OpenStreetMap, Mapbox)
|
||||
- Embedding iframes from external services (YouTube, Vimeo)
|
||||
- Loading external stylesheets or scripts
|
||||
|
||||
Salesforce enforces Content Security Policy (CSP) headers on all web applications. Any external domain not registered as a CSP Trusted Site will be blocked by the browser, causing images to not load, API calls to fail, or fonts to be missing.
|
||||
|
||||
**Reference:** [Salesforce CspTrustedSite Object Reference](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_csptrustedsite.htm)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Identify external domains
|
||||
|
||||
Scan the code for any URLs pointing to external domains. Common patterns:
|
||||
|
||||
- `fetch("https://api.example.com/...")` — API calls
|
||||
- `<img src="https://images.example.com/..." />` — images
|
||||
- `<link href="https://fonts.example.com/..." />` — stylesheets
|
||||
- `url="https://tiles.example.com/{z}/{x}/{y}.png"` — map tiles
|
||||
- `@import url("https://cdn.example.com/...")` — CSS imports
|
||||
|
||||
Extract the **origin** (scheme + host) from each URL. For example:
|
||||
- `https://api.open-meteo.com/v1/forecast?lat=...` → `https://api.open-meteo.com`
|
||||
- `https://images.unsplash.com/photo-123?w=800` → `https://images.unsplash.com`
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Check existing CSP Trusted Sites
|
||||
|
||||
Before creating a new file, check if the domain already has a CSP Trusted Site:
|
||||
|
||||
```bash
|
||||
ls force-app/main/default/cspTrustedSites/
|
||||
```
|
||||
|
||||
If the domain is already registered, no action is needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Determine the CSP directive(s)
|
||||
|
||||
Map the resource type to the correct CSP `isApplicableTo*Src` fields. Read `implementation/metadata-format.md` for the full reference.
|
||||
|
||||
Quick reference:
|
||||
|
||||
| Resource type | CSP directive field(s) to set `true` |
|
||||
|--------------|--------------------------------------|
|
||||
| Images (img, background-image) | `isApplicableToImgSrc` |
|
||||
| API calls (fetch, XMLHttpRequest) | `isApplicableToConnectSrc` |
|
||||
| Fonts (.woff, .woff2, .ttf) | `isApplicableToFontSrc` |
|
||||
| Stylesheets (CSS) | `isApplicableToStyleSrc` |
|
||||
| Video / audio | `isApplicableToMediaSrc` |
|
||||
| Iframes | `isApplicableToFrameSrc` |
|
||||
|
||||
**Always also set `isApplicableToConnectSrc` to `true`** — most resources also require connect-src for preflight/redirect handling.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create the metadata file
|
||||
|
||||
Read `implementation/metadata-format.md` and follow the instructions to create the `.cspTrustedSite-meta.xml` file.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify
|
||||
|
||||
1. Confirm the file is valid XML and matches the expected schema.
|
||||
2. Confirm the file is placed in `force-app/main/default/cspTrustedSites/`.
|
||||
3. Confirm only the necessary `isApplicableTo*Src` fields are set to `true`.
|
||||
4. Run from the web app directory:
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
||||
```
|
||||
|
||||
- **Lint:** MUST result in 0 errors.
|
||||
- **Build:** MUST succeed.
|
||||
@ -0,0 +1,281 @@
|
||||
# CSP Trusted Site Metadata — Implementation Guide
|
||||
|
||||
## File location
|
||||
|
||||
```
|
||||
force-app/main/default/cspTrustedSites/{Name}.cspTrustedSite-meta.xml
|
||||
```
|
||||
|
||||
The `cspTrustedSites/` directory must be a direct child of `force-app/main/default/`. Create it if it does not exist.
|
||||
|
||||
---
|
||||
|
||||
## File naming convention
|
||||
|
||||
The file name must match the `<fullName>` value inside the XML, with `.cspTrustedSite-meta.xml` appended.
|
||||
|
||||
| Domain | fullName | File name |
|
||||
|--------|----------|-----------|
|
||||
| `https://images.unsplash.com` | `Unsplash_Images` | `Unsplash_Images.cspTrustedSite-meta.xml` |
|
||||
| `https://api.open-meteo.com` | `Open_Meteo_API` | `Open_Meteo_API.cspTrustedSite-meta.xml` |
|
||||
| `https://tile.openstreetmap.org` | `OpenStreetMap_Tiles` | `OpenStreetMap_Tiles.cspTrustedSite-meta.xml` |
|
||||
|
||||
**Naming rules:**
|
||||
- Use PascalCase with underscores separating words (e.g. `Google_Fonts_Static`)
|
||||
- Name should describe the provider and resource type (e.g. `Pexels_Videos`, not just `Pexels`)
|
||||
- Must be unique across the org
|
||||
- Maximum 80 characters
|
||||
|
||||
---
|
||||
|
||||
## XML template
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>{UNIQUE_NAME}</fullName>
|
||||
<description>{DESCRIPTION}</description>
|
||||
<endpointUrl>{HTTPS_ORIGIN}</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>{true|false}</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>{true|false}</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>{true|false}</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>{true|false}</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>{true|false}</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>{true|false}</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field reference
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `fullName` | Yes | Unique API name. Must match the file name (before `.cspTrustedSite-meta.xml`). |
|
||||
| `description` | Yes | Human-readable purpose. Start with "Allow access to..." |
|
||||
| `endpointUrl` | Yes | The external origin (scheme + host). Must start with `https://`. No trailing slash. No path. |
|
||||
| `isActive` | Yes | Always `true` for new entries. Set `false` to disable without deleting. |
|
||||
| `context` | Yes | `All` (applies to all contexts). Other values: `LEX` (Lightning Experience only), `Communities` (Experience Cloud only), `VisualForce`. Use `All` unless there is a specific reason to restrict. |
|
||||
| `isApplicableToConnectSrc` | Yes | `true` if the domain is called via `fetch()`, `XMLHttpRequest`, or WebSocket. |
|
||||
| `isApplicableToFontSrc` | Yes | `true` if the domain serves font files (`.woff`, `.woff2`, `.ttf`, `.otf`). |
|
||||
| `isApplicableToFrameSrc` | Yes | `true` if the domain is loaded in an `<iframe>` or `<object>`. |
|
||||
| `isApplicableToImgSrc` | Yes | `true` if the domain serves images (`<img>`, CSS `background-image`, `<svg>`). |
|
||||
| `isApplicableToMediaSrc` | Yes | `true` if the domain serves audio or video (`<audio>`, `<video>`). |
|
||||
| `isApplicableToStyleSrc` | Yes | `true` if the domain serves CSS stylesheets (`<link rel="stylesheet">`). |
|
||||
|
||||
**Reference:** [CspTrustedSite — Salesforce Object Reference](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_csptrustedsite.htm)
|
||||
|
||||
---
|
||||
|
||||
## CSP directive mapping
|
||||
|
||||
| CSP header directive | Metadata field | What it allows |
|
||||
|---------------------|----------------|----------------|
|
||||
| `connect-src` | `isApplicableToConnectSrc` | `fetch()`, `XMLHttpRequest`, WebSocket, `EventSource` |
|
||||
| `font-src` | `isApplicableToFontSrc` | `@font-face` sources |
|
||||
| `frame-src` | `isApplicableToFrameSrc` | `<iframe>`, `<frame>`, `<object>`, `<embed>` |
|
||||
| `img-src` | `isApplicableToImgSrc` | `<img>`, `background-image`, `favicon`, `<picture>` |
|
||||
| `media-src` | `isApplicableToMediaSrc` | `<audio>`, `<video>`, `<source>`, `<track>` |
|
||||
| `style-src` | `isApplicableToStyleSrc` | `<link rel="stylesheet">`, `@import` in CSS |
|
||||
|
||||
---
|
||||
|
||||
## Common external domains and their directives
|
||||
|
||||
Use this table as a quick reference when adding new domains:
|
||||
|
||||
| Domain | connect-src | font-src | frame-src | img-src | media-src | style-src |
|
||||
|--------|:-----------:|:--------:|:---------:|:-------:|:---------:|:---------:|
|
||||
| `https://images.unsplash.com` | true | false | false | true | false | false |
|
||||
| `https://images.pexels.com` | true | false | false | true | false | false |
|
||||
| `https://videos.pexels.com` | true | false | false | false | true | false |
|
||||
| `https://fonts.googleapis.com` | true | false | false | false | false | true |
|
||||
| `https://fonts.gstatic.com` | true | true | false | false | false | false |
|
||||
| `https://avatars.githubusercontent.com` | true | false | false | true | false | false |
|
||||
| `https://api.open-meteo.com` | true | false | false | false | false | false |
|
||||
| `https://nominatim.openstreetmap.org` | true | false | false | false | false | false |
|
||||
| `https://tile.openstreetmap.org` | true | false | false | true | false | false |
|
||||
| `https://api.mapbox.com` | true | false | false | true | false | false |
|
||||
| `https://cdn.jsdelivr.net` | true | false | false | false | false | true |
|
||||
| `https://www.youtube.com` | false | false | true | true | false | false |
|
||||
| `https://player.vimeo.com` | false | false | true | false | false | false |
|
||||
| `https://res.cloudinary.com` | true | false | false | true | false | false |
|
||||
|
||||
---
|
||||
|
||||
## Complete examples
|
||||
|
||||
### Image CDN (Unsplash)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Unsplash_Images</fullName>
|
||||
<description>Allow access to Unsplash image content for static app media</description>
|
||||
<endpointUrl>https://images.unsplash.com</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>false</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>true</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>false</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
### REST API (Open-Meteo weather)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Open_Meteo_API</fullName>
|
||||
<description>Allow access to Open-Meteo weather forecast API</description>
|
||||
<endpointUrl>https://api.open-meteo.com</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>false</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>false</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>false</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
### Font provider (Google Fonts — requires two entries)
|
||||
|
||||
Google Fonts needs two CSP entries because CSS is served from `fonts.googleapis.com` and font files from `fonts.gstatic.com`:
|
||||
|
||||
**Entry 1: Stylesheets**
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Google_Fonts</fullName>
|
||||
<description>Allow access to Google Fonts stylesheets for custom typography</description>
|
||||
<endpointUrl>https://fonts.googleapis.com</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>false</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>false</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>true</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
**Entry 2: Font files**
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Google_Fonts_Static</fullName>
|
||||
<description>Allow access to Google Fonts static files for font loading</description>
|
||||
<endpointUrl>https://fonts.gstatic.com</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>true</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>false</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>false</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
### Map tiles (OpenStreetMap)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>OpenStreetMap_Tiles</fullName>
|
||||
<description>Allow access to OpenStreetMap tile images for map rendering</description>
|
||||
<endpointUrl>https://tile.openstreetmap.org</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>false</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>true</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>false</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
### Geocoding API (Nominatim)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<CspTrustedSite xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>OpenStreetMap_Nominatim</fullName>
|
||||
<description>Allow access to OpenStreetMap Nominatim geocoding API</description>
|
||||
<endpointUrl>https://nominatim.openstreetmap.org</endpointUrl>
|
||||
<isActive>true</isActive>
|
||||
<context>All</context>
|
||||
<isApplicableToConnectSrc>true</isApplicableToConnectSrc>
|
||||
<isApplicableToFontSrc>false</isApplicableToFontSrc>
|
||||
<isApplicableToFrameSrc>false</isApplicableToFrameSrc>
|
||||
<isApplicableToImgSrc>false</isApplicableToImgSrc>
|
||||
<isApplicableToMediaSrc>false</isApplicableToMediaSrc>
|
||||
<isApplicableToStyleSrc>false</isApplicableToStyleSrc>
|
||||
</CspTrustedSite>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoint URL rules
|
||||
|
||||
| Rule | Correct | Incorrect |
|
||||
|------|---------|-----------|
|
||||
| Must be HTTPS | `https://api.example.com` | `http://api.example.com` |
|
||||
| No trailing slash | `https://api.example.com` | `https://api.example.com/` |
|
||||
| No path | `https://api.example.com` | `https://api.example.com/v1/forecast` |
|
||||
| No port (unless non-standard) | `https://api.example.com` | `https://api.example.com:443` |
|
||||
| No wildcards | `https://api.example.com` | `https://*.example.com` |
|
||||
|
||||
Each subdomain needs its own entry. For example, `fonts.googleapis.com` and `fonts.gstatic.com` are separate entries.
|
||||
|
||||
---
|
||||
|
||||
## When a service requires multiple domains
|
||||
|
||||
Some services split resources across multiple subdomains. Create one CSP Trusted Site per domain:
|
||||
|
||||
| Service | Domains needed |
|
||||
|---------|---------------|
|
||||
| Google Fonts | `fonts.googleapis.com` (CSS) + `fonts.gstatic.com` (font files) |
|
||||
| Mapbox | `api.mapbox.com` (tiles/API) + `events.mapbox.com` (telemetry) |
|
||||
| YouTube embed | `www.youtube.com` (iframe) + `i.ytimg.com` (thumbnails) |
|
||||
| Cloudflare CDN | `cdnjs.cloudflare.com` (scripts/CSS) |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting CSP violations
|
||||
|
||||
If the browser console shows a CSP error like:
|
||||
|
||||
```
|
||||
Refused to load the image 'https://example.com/image.png' because it violates
|
||||
the following Content Security Policy directive: "img-src 'self' ..."
|
||||
```
|
||||
|
||||
1. Extract the **blocked origin** from the URL (e.g. `https://example.com`).
|
||||
2. Identify the **directive** from the error message (e.g. `img-src` → `isApplicableToImgSrc`).
|
||||
3. Check if a CSP Trusted Site already exists for that origin.
|
||||
4. If not, create one using this skill.
|
||||
5. Deploy the metadata and refresh the page.
|
||||
|
||||
---
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Including a path in `endpointUrl` | Use only the origin: `https://api.example.com` |
|
||||
| Adding trailing slash | Remove it: `https://api.example.com` not `https://api.example.com/` |
|
||||
| Using HTTP instead of HTTPS | Salesforce requires HTTPS. If the service only supports HTTP, it cannot be added. |
|
||||
| Forgetting `isApplicableToConnectSrc` | Most resources also need connect-src for redirects/preflight. Set to `true` by default. |
|
||||
| One entry for multiple subdomains | Each subdomain needs its own file (e.g. `api.example.com` and `cdn.example.com` are separate) |
|
||||
| File name doesn't match `fullName` | They must be identical (excluding the `.cspTrustedSite-meta.xml` extension) |
|
||||
158
skills/configuring-webapp-metadata/SKILL.md
Normal file
158
skills/configuring-webapp-metadata/SKILL.md
Normal file
@ -0,0 +1,158 @@
|
||||
---
|
||||
name: configuring-webapp-metadata
|
||||
description: Use this skill when configuring web application metadata structure, webapplication.json, or bundle organization. Covers WebApplication bundle layout, meta XML, build output directory, and webapplication.json settings.
|
||||
---
|
||||
|
||||
# WebApplication Requirements
|
||||
|
||||
## Bundle Rules
|
||||
- A WebApplication bundle must live under `webapplications/<AppName>/`
|
||||
- The bundle must contain `<AppName>.webapplication-meta.xml`
|
||||
- The metadata filename must exactly match the folder name
|
||||
- A build output directory must exist and contain at least one file
|
||||
- Default build output directory: `dist/`
|
||||
- If `webapplication.json.outputDir` is set, it overrides `dist/`
|
||||
|
||||
Valid example:
|
||||
```text
|
||||
webapplications/
|
||||
MyApp/
|
||||
MyApp.webapplication-meta.xml
|
||||
webapplication.json
|
||||
dist/
|
||||
index.html
|
||||
```
|
||||
|
||||
## Metadata XML
|
||||
Required fields:
|
||||
- `masterLabel`
|
||||
- `version` (max 20 chars)
|
||||
- `isActive` (boolean)
|
||||
|
||||
Optional fields:
|
||||
- `description` (max 255 chars)
|
||||
|
||||
## webapplication.json
|
||||
`webapplication.json` is optional.
|
||||
|
||||
Allowed top-level keys only:
|
||||
- `outputDir`
|
||||
- `routing`
|
||||
- `headers`
|
||||
|
||||
### File Constraints
|
||||
- Must be valid UTF-8 JSON
|
||||
- Max size: 100 KB
|
||||
- Root must be a non-empty object
|
||||
- Never allow `{}`, arrays, or primitives as the root
|
||||
|
||||
### Path Safety
|
||||
Applies to:
|
||||
- `outputDir`
|
||||
- `routing.fallback`
|
||||
|
||||
Reject:
|
||||
- backslashes
|
||||
- leading `/` or `\`
|
||||
- `..` segments
|
||||
- null or control characters
|
||||
- globs: `*`, `?`, `**`
|
||||
- `%`
|
||||
|
||||
All resolved paths must stay within the application bundle.
|
||||
|
||||
### outputDir
|
||||
- Must be a non-empty string
|
||||
- Must reference a subdirectory only
|
||||
- Reject `.` and `./`
|
||||
- The directory must exist in the bundle
|
||||
- The directory must contain at least one file
|
||||
|
||||
### routing
|
||||
- If present, must be a non-empty object
|
||||
- Allowed keys only:
|
||||
- `rewrites`
|
||||
- `redirects`
|
||||
- `fallback`
|
||||
- `trailingSlash`
|
||||
- `fileBasedRouting`
|
||||
|
||||
#### routing.trailingSlash
|
||||
- Must be one of: `"always"`, `"never"`, `"auto"`
|
||||
|
||||
#### routing.fileBasedRouting
|
||||
- Must be a boolean
|
||||
|
||||
#### routing.fallback
|
||||
- Must be a non-empty string
|
||||
- Must satisfy Path Safety rules
|
||||
- Target file must exist
|
||||
|
||||
#### routing.rewrites
|
||||
- Must be a non-empty array
|
||||
- Each item must be a non-empty object
|
||||
- Allowed keys: `route`, `rewrite`
|
||||
- `rewrite` must be a non-empty string
|
||||
- `route`, if present, must be a non-empty string
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"rewrites": [
|
||||
{ "route": "/app/:path*", "rewrite": "/index.html" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### routing.redirects
|
||||
- Must be a non-empty array
|
||||
- Each item must be a non-empty object
|
||||
- Allowed keys: `route`, `redirect`, `statusCode`
|
||||
- `redirect` must be a non-empty string
|
||||
- `route`, if present, must be a non-empty string
|
||||
- `statusCode`, if present, must be one of: `301`, `302`, `307`, `308`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"redirects": [
|
||||
{ "route": "/old-page", "redirect": "/new-page", "statusCode": 301 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### headers
|
||||
- If present, must be a non-empty array
|
||||
- Each item must be a non-empty object
|
||||
- Allowed keys: `source`, `headers`
|
||||
- `headers` must be a non-empty array
|
||||
|
||||
Each header entry must contain:
|
||||
- `key`: non-empty string
|
||||
- `value`: non-empty string
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/assets/**",
|
||||
"headers": [
|
||||
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Never Suggest
|
||||
- `{}` as the JSON root
|
||||
- `"routing": {}`
|
||||
- empty arrays
|
||||
- empty array items such as `[{}]`
|
||||
- `"outputDir": "."`
|
||||
- `"outputDir": "./"`
|
||||
141
skills/creating-webapp/SKILL.md
Normal file
141
skills/creating-webapp/SKILL.md
Normal file
@ -0,0 +1,141 @@
|
||||
---
|
||||
name: creating-webapp
|
||||
description: Use this skill when creating or setting up a new SFDX React web application. Covers first steps, npm install, skills-first protocol, deployment order, and core web app rules.
|
||||
paths:
|
||||
- "**/webapplications/**/*"
|
||||
---
|
||||
|
||||
# First Steps (MUST FOLLOW)
|
||||
|
||||
**Always run `npm install` before doing anything else** when working in a web app directory (e.g. `force-app/main/default/webapplications/<appName>/` or a dist app path). Dependencies must be installed before running `npm run dev`, `npm run build`, `npm run lint`, or any other script. If `node_modules` is missing or stale, commands will fail.
|
||||
|
||||
# Skills-First (MUST FOLLOW)
|
||||
|
||||
**Before writing any code or running any command**, search for relevant skills (`SKILL.md` files) that cover your task. Read the full skill and follow its instructions. Skills live in `.a4drules/skills/` and `feature/*/skills/`.
|
||||
|
||||
- Do not write custom scripts or complex bash commands for a workflow already covered by a loaded skill.
|
||||
- Only proceed with manual execution after confirming no relevant skill exists.
|
||||
|
||||
# Deployment Order (MUST FOLLOW)
|
||||
|
||||
**Metadata deployments must complete before fetching GraphQL schema or running codegen.** The schema reflects the current org state; custom objects and fields appear only after metadata is deployed. Running schema fetch or codegen too early produces incomplete or incorrect types.
|
||||
|
||||
**Invoke the `deploying-to-salesforce` skill** (`.a4drules/skills/deploying-to-salesforce/`) whenever the task involves:
|
||||
- Deploying metadata (objects, permission sets, layouts)
|
||||
- Fetching GraphQL schema (`npm run graphql:schema`)
|
||||
- Running GraphQL codegen (`npm run graphql:codegen`)
|
||||
- Generating deploy/setup commands or syncing with the org
|
||||
|
||||
The skill enforces the correct sequence: **deploy metadata → assign permset → schema fetch → codegen**.
|
||||
|
||||
**Critical rules:**
|
||||
- Do **not** run `npm run graphql:schema` before metadata (objects, permission sets) is deployed — the schema will not include custom objects/fields.
|
||||
- Do **not** skip schema refetch after any metadata deployment — re-run `npm run graphql:schema` and `npm run graphql:codegen` from the webapp dir.
|
||||
|
||||
# Web App Generation
|
||||
|
||||
## Before `sf webapp generate`
|
||||
|
||||
**Webapp name (`-n`):** Must be **alphanumerical only**—no spaces, hyphens, underscores, or special characters. Use only letters (A–Z, a–z) and digits (0–9). Example: `CoffeeBoutique` not `Coffee Boutique`.
|
||||
|
||||
```bash
|
||||
sf webapp generate -n MyWebApp -t reactbasic
|
||||
```
|
||||
|
||||
Do not use `create-react-app`, Vite, or other generic scaffolds; use `sf webapp generate` so the app is SFDX-aware.
|
||||
|
||||
## After Generation (MANDATORY)
|
||||
|
||||
After generating or when touching an existing app:
|
||||
|
||||
1. **Replace all default boilerplate** — "React App", "Vite + React", default `<title>`, placeholder text in shell. Use the actual app name.
|
||||
2. **Populate the home page** — Never leave it as default template. Add real content: landing section, banners, hero, navigation to features.
|
||||
3. **Update navigation and placeholders** — See [Navigation & Layout section](#navigation--layout-mandatory) below.
|
||||
|
||||
# Navigation & Layout (MANDATORY)
|
||||
|
||||
Agents consistently miss these. **You must not leave them default.**
|
||||
|
||||
## appLayout.tsx is the Source of Truth
|
||||
|
||||
- **Build navigation into the app layout** (`appLayout.tsx`). The layout must include nav (header, sidebar, or both) so every page shares the same shell.
|
||||
- Path: `force-app/main/default/webapplications/<appName>/src/appLayout.tsx`
|
||||
|
||||
## When Making UI Changes
|
||||
|
||||
**When making any change** that affects navigation, header, footer, sidebar, theme, or overall layout:
|
||||
|
||||
1. **You MUST edit `src/appLayout.tsx`** (the layout used by `routes.tsx`).
|
||||
2. Do not only edit pages/components and leave `appLayout.tsx` unchanged.
|
||||
3. Before finishing: confirm you opened and modified `appLayout.tsx`. If you did not, the task is incomplete.
|
||||
|
||||
## Navigation Menu (Critical)
|
||||
|
||||
- **Always edit the navigation menu** in `appLayout.tsx`. Replace default nav items and labels with **app-specific** links and names.
|
||||
- Do **not** leave template items (e.g. "Home", "About", generic placeholder links).
|
||||
- Use real routes and labels matching the app (e.g. "Dashboard", "Products", "Orders").
|
||||
|
||||
**Check before finishing:** Did I change the nav items and labels to match this app?
|
||||
|
||||
## Placeholder Name & Design (Critical)
|
||||
|
||||
- **Replace the placeholder app name** everywhere: header, nav brand/logo, footer, `<title>` in `index.html`, any "Welcome to…" text.
|
||||
- **Replace placeholder design** in the shell: default header/footer styling, generic branding.
|
||||
|
||||
**Check before finishing:** Is the app name and shell design still the template default? If yes, update it.
|
||||
|
||||
## Where to Edit
|
||||
|
||||
|
||||
| What | Where |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| Layout/nav/branding | `force-app/main/default/webapplications/<appName>/src/appLayout.tsx` |
|
||||
| Document title | `force-app/main/default/webapplications/<appName>/index.html` |
|
||||
| Root page content | Component at root route (often `Home` in `routes.tsx`) |
|
||||
|
||||
|
||||
# React & TypeScript Constraints
|
||||
|
||||
## Routing (React Router)
|
||||
|
||||
Use a **single** router package. When using `createBrowserRouter` / `RouterProvider`, all imports MUST come from **`react-router`** — not `react-router-dom`.
|
||||
|
||||
## Component Library + Styling
|
||||
|
||||
- **shadcn/ui** for components: `import { Button } from '@/components/ui/button';`
|
||||
- **Tailwind CSS** utility classes
|
||||
|
||||
## URL & Path Handling
|
||||
|
||||
Apps run behind dynamic base paths. Router navigation (`<Link to>`, `navigate()`) prefer absolute paths (`/x`). Non-router attributes (`<img src>`) use dot-relative (`./x`) to resolve against `<base>`. Prefer Vite `import` for static assets.
|
||||
|
||||
## Module Restrictions
|
||||
|
||||
React apps must NOT import Salesforce platform modules like `lightning/*` or `@wire` (LWC-only). For data access, invoke the **accessing-data** skill.
|
||||
|
||||
# Frontend Aesthetics
|
||||
|
||||
**Avoid AI slop.** Make creative, distinctive frontends:
|
||||
|
||||
- **Typography:** Avoid Inter, Roboto, Arial, Space Grotesk as defaults. Choose distinctive fonts.
|
||||
- **Color:** Use cohesive color with sharp accents via CSS variables. Avoid purple-on-white clichés.
|
||||
- **Motion:** Use high-impact motion (e.g. staggered reveals).
|
||||
- **Depth:** Add atmosphere/depth in backgrounds.
|
||||
|
||||
# Shell Command Safety (MUST FOLLOW)
|
||||
|
||||
**Never use complex `node -e` one-liners** for file edits or multi-line transforms. They break in Zsh due to `!` history expansion and backtick interpolation. Use a temporary `.js` file, `sed`/`awk`, `jq`, or IDE file-editing tools instead.
|
||||
|
||||
# Development Cycle
|
||||
|
||||
- Execute tasks continuously until all planned items complete in the current iteration.
|
||||
- Maintain a running checklist and proceed sequentially.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Only stop when:
|
||||
|
||||
- All checklist items are completed and quality gates pass, or
|
||||
- A blocking error cannot be resolved after reasonable remediation, or
|
||||
- The user explicitly asks to pause.
|
||||
|
||||
229
skills/deploying-webapp-to-salesforce/SKILL.md
Normal file
229
skills/deploying-webapp-to-salesforce/SKILL.md
Normal file
@ -0,0 +1,229 @@
|
||||
---
|
||||
name: deploying-webapp-to-salesforce
|
||||
description: Enforces the correct order for deploying metadata, assigning permission sets, and fetching GraphQL schema. Use for ANY deployment to a Salesforce org — webapps, LWC, Aura, Apex, metadata, schema fetch, or org sync. Codifies setup-cli.mjs.
|
||||
paths:
|
||||
- "**/*"
|
||||
---
|
||||
|
||||
# Deploying to Salesforce
|
||||
|
||||
Guidance for AI agents deploying metadata to a Salesforce org or syncing with the org. **The order of operations is critical.** This skill codifies the exact sequence from `scripts/setup-cli.mjs` and documents **every Salesforce interaction**.
|
||||
|
||||
## When to Use
|
||||
|
||||
Invoke this skill whenever the task involves:
|
||||
|
||||
- Deploying metadata (objects, permission sets, layouts, Apex, web applications)
|
||||
- Generating deploy commands or setup instructions
|
||||
- Fetching the GraphQL schema (`npm run graphql:schema`)
|
||||
- Running GraphQL codegen (`npm run graphql:codegen`)
|
||||
- Full org setup (login, deploy, permset, data, schema, build)
|
||||
- Any manual step that touches the Salesforce org
|
||||
|
||||
## Canonical Sequence (from setup-cli.mjs)
|
||||
|
||||
Execute steps in this **exact order**. Steps marked **(SF)** perform a Salesforce API or CLI interaction.
|
||||
|
||||
### Step 1: Login — org authentication
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Check if org is connected | **(SF)** | `sf org display --target-org <alias> --json` |
|
||||
| If not connected: authenticate | **(SF)** | `sf org login web --alias <alias>` |
|
||||
|
||||
- **Run when:** Org is not connected. **Omit when:** Org is already authenticated (check via `sf org display`).
|
||||
- All subsequent steps require an authenticated org.
|
||||
|
||||
### Step 2: Webapp build — pre-deploy (required for entity deployment)
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Install dependencies | No | `npm install` (in each webapp dir) |
|
||||
| Build web app | No | `npm run build` (in each webapp dir) |
|
||||
|
||||
- Produces `dist/` so `sf project deploy start` can deploy web application entities. Run **before** deploy when deploying web apps.
|
||||
- **Run when:** Deploying web apps AND (`dist/` does not exist OR webapp source has changed since last build). **Omit when:** Not deploying, or `dist/` is current and no source changes.
|
||||
|
||||
### Step 3: Deploy metadata
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Deploy metadata | **(SF)** | See below |
|
||||
|
||||
**Check for a manifest (package.xml) first.** Only use it if present:
|
||||
|
||||
- **If `manifest/package.xml` (or `package.xml`) exists:** Deploy using the manifest:
|
||||
```bash
|
||||
sf project deploy start --manifest manifest/package.xml --target-org <alias>
|
||||
```
|
||||
- **If no manifest exists:** Deploy all metadata from the project (packageDirectories in sfdx-project.json):
|
||||
```bash
|
||||
sf project deploy start --target-org <alias>
|
||||
```
|
||||
|
||||
Do not assume a manifest exists. Check the project root and common locations (e.g., `manifest/`, `config/`) before choosing the deploy command.
|
||||
|
||||
- Deploys objects, layouts, permission sets, Apex classes, web applications, and all other metadata.
|
||||
- **Must complete successfully before schema fetch** — the schema reflects org state; custom objects/fields appear only after deployment.
|
||||
- **Run when:** Metadata has changed since last deploy, or never deployed. **Omit when:** No metadata changes and deploy has already run successfully.
|
||||
|
||||
### Step 4: Post-deployment configuration — assign permissions and configure
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Assign permission set or group | **(SF)** | `sf org assign permset --name <name> --target-org <alias>` (works for both permsets and permset groups) |
|
||||
| Assign profile to user | **(SF)** | `sf data update record` or at user creation |
|
||||
| Other post-deploy config | **(SF)** | Varies (e.g., named credentials, connected apps, custom settings) |
|
||||
|
||||
**Example commands:**
|
||||
|
||||
```bash
|
||||
# Permission set (assigns to default user of target org)
|
||||
sf org assign permset --name Property_Management_Access --target-org myorg
|
||||
|
||||
# Permission set group (same command; pass the group name)
|
||||
sf org assign permset --name My_Permset_Group --target-org myorg
|
||||
|
||||
# Assign to a specific user
|
||||
sf org assign permset --name Property_Management_Access --target-org myorg --on-behalf-of user@example.com
|
||||
|
||||
# Profile — update existing user's profile (requires ProfileId and User Id)
|
||||
sf data update record --sobject User --record-id <userId> --values "ProfileId=<profileId>" --target-org myorg
|
||||
|
||||
# Profile — get ProfileId first
|
||||
sf data query --query "SELECT Id, Name FROM Profile WHERE Name='Standard User'" --target-org myorg
|
||||
```
|
||||
|
||||
- **Deploying does not mean assigning.** Even after permission sets, permission set groups, and profiles are deployed to the org, they must be explicitly assigned or configured for users. Deployment makes them available; assignment/configuration grants access.
|
||||
- **Permission sets** — Assign to users so they have access to custom objects and fields. Required for GraphQL introspection to return the correct schema.
|
||||
- **Permission set groups** — Assign to users when using grouped permission sets.
|
||||
- **Profiles** — Ensure users have the correct profile; profile assignment may be done at user creation or via Setup.
|
||||
- **Other post-deploy configuration** — Named credentials, connected apps, custom settings, flow activation, and any metadata that requires manual configuration after deploy.
|
||||
- All of the above must exist in the org (deployed in Step 3).
|
||||
- **Run when:** Any permission set, permission set group, profile, or other post-deploy config was deployed or changed and not yet assigned/configured. **Omit when:** All required assignments and configuration are already in place.
|
||||
|
||||
**Proactive behavior:** After a successful deploy, discover permission sets in the project (e.g., `force-app/main/default/permissionsets/*.permissionset-meta.xml`) and assign each one. Extract the API name from the filename (e.g., `Property_Management_Access.permissionset-meta.xml` → `Property_Management_Access`). If the user has not explicitly requested a full setup, you may ask: "Do you want me to assign the permission sets to your org?" — but do not skip this step silently; either run it or ask.
|
||||
|
||||
### Step 5: Data — prepare and import (optional)
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Prepare unique fields | No | `node scripts/prepare-import-unique-fields.js --data-dir <dataDir>` |
|
||||
| Clean existing records | **(SF)** | `sf apex run --target-org <alias> --file <apex>` (per sobject, reverse plan order) |
|
||||
| Import records | **(SF)** | `sf apex run --target-org <alias> --file <apex>` (per batch) |
|
||||
|
||||
- Only runs if `data/data-plan.json` and `data/` exist.
|
||||
- Delete runs in reverse plan order (children before parents).
|
||||
- Import uses Anonymous Apex with `Database.DMLOptions.duplicateRuleHeader.allowSave = true`.
|
||||
- **Run when:** Data plan exists AND (never imported OR data files or plan changed). **Omit when:** No data plan, or data already imported and unchanged.
|
||||
|
||||
**Proactive behavior:** If `data/data-plan.json` and `data/` exist, you MUST ask the user: "Do you want me to import the sample data now? This will prepare unique fields, optionally clean existing records, and import data. Reply yes to proceed." Do not skip this step silently; either run it (after confirmation) or ask. Never import or clean without explicit user confirmation.
|
||||
|
||||
### Step 6: GraphQL schema and codegen
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Install webapp deps | No | `npm install` (in webapp dir) |
|
||||
| Set default org | **(SF)** | `sf config set target-org <alias> --global` |
|
||||
| Fetch schema (introspection) | **(SF)** | `npm run graphql:schema` (from webapp dir) |
|
||||
| Generate types | No | `npm run graphql:codegen` (from webapp dir) |
|
||||
|
||||
- `graphql:schema` performs GraphQL introspection against the org — a **Salesforce API call**.
|
||||
- Schema is written to `schema.graphql` at the SFDX project root.
|
||||
- Codegen reads the schema file locally; no Salesforce interaction.
|
||||
- **Run when:** Schema does not exist, OR metadata was deployed/changed since last schema fetch, OR permissions or post-deploy config was assigned since last schema fetch. **Omit when:** Schema exists and is current relative to org state.
|
||||
|
||||
### Step 7: Webapp build (if not done in Step 2)
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Build web app | No | `npm run build` (in webapp dir) |
|
||||
|
||||
- **Run when:** Build is needed (e.g., for dev server or deploy) AND (`dist/` does not exist OR webapp source has changed since last build). **Omit when:** `dist/` is current and no build needed.
|
||||
|
||||
### Step 8: Dev server (optional)
|
||||
|
||||
| Action | Salesforce interaction? | Command |
|
||||
|--------|-------------------------|---------|
|
||||
| Launch dev server | No | `npm run dev` (in webapp dir) |
|
||||
|
||||
- **Run when:** User requests to launch the dev server. **Omit when:** Not requested.
|
||||
|
||||
## Summary: All Salesforce Interactions (in order)
|
||||
|
||||
1. `sf org display` — check org connection
|
||||
2. `sf org login web` — authenticate (if needed)
|
||||
3. `sf project deploy start` — deploy metadata
|
||||
4. `sf org assign permset` (permsets and permset groups) / profile assignment / other post-deploy config — assign permissions and configure
|
||||
5. `sf apex run` — delete existing data (if data plan)
|
||||
6. `sf apex run` — import data (if data plan)
|
||||
7. `sf config set target-org` — set default org for schema
|
||||
8. `npm run graphql:schema` — GraphQL introspection (Salesforce API)
|
||||
|
||||
## Post-Deploy Checklist (MUST NOT SKIP)
|
||||
|
||||
After **every successful metadata deploy**, the agent MUST address these before considering the task complete:
|
||||
|
||||
1. **Permission sets** — Discover `force-app/main/default/permissionsets/*.permissionset-meta.xml`, extract API names, and assign each via `sf org assign permset --name <name> --target-org <alias>`. If unsure, ask: "Do you want me to assign the permission sets (e.g., Property_Management_Access, Tenant_Maintenance_Access) to your org?"
|
||||
2. **Data import** — If `data/data-plan.json` exists, ask: "Do you want me to import the sample data now? Reply yes to proceed." Do not import without confirmation.
|
||||
3. **Schema refetch** — Run `npm run graphql:schema` and `npm run graphql:codegen` from the webapp dir (required after deploy).
|
||||
|
||||
Do not silently skip permission set assignment or data import. Either run them or ask the user.
|
||||
|
||||
## Agent Decision Criteria
|
||||
|
||||
Evaluate each step before running it. Ask:
|
||||
|
||||
- **Has this step ever run before?** If not, it is likely needed.
|
||||
- **Have changes been made that require this step?** (e.g., metadata edits → deploy; deploy → schema refetch)
|
||||
- **Is the current state sufficient?** (e.g., org connected, schema exists and is current, permissions and post-deploy config assigned)
|
||||
|
||||
Do not rely on CLI flags. Decide based on project state and what has changed.
|
||||
|
||||
## Evaluation: When Each Step Touches Salesforce
|
||||
|
||||
| Step | Salesforce interaction | Run when |
|
||||
|------|------------------------|----------|
|
||||
| 1. Login | Yes | Org not connected |
|
||||
| 2. Webapp build | No | Deploying web apps AND dist missing or source changed |
|
||||
| 3. Deploy | Yes | Metadata changed or never deployed |
|
||||
| 4. Post-deploy config | Yes | Permission sets, permset groups, profiles, or other config deployed/changed and not assigned |
|
||||
| 5. Data | Yes | Data plan exists AND (never imported OR data changed) |
|
||||
| 6. GraphQL | Yes (schema only) | Schema missing or metadata/permissions changed since last fetch |
|
||||
| 7. Webapp build | No | Build needed AND dist missing or source changed |
|
||||
| 8. Dev | No | User requests dev server |
|
||||
|
||||
**Critical rule:** Steps 3 (deploy) and 4 (post-deploy config) must complete **before** step 6 (graphql:schema). The schema reflects the org state; running introspection too early yields an incomplete schema.
|
||||
|
||||
## Schema Refetch Rule (CRITICAL)
|
||||
|
||||
**After any metadata deployment**, you MUST re-run schema fetch and codegen:
|
||||
|
||||
- New custom objects
|
||||
- New custom fields
|
||||
- New or updated permission sets
|
||||
- Any change to metadata that affects the GraphQL schema
|
||||
|
||||
```bash
|
||||
# From webapp dir (force-app/main/default/webapplications/<appName>/)
|
||||
npm run graphql:schema
|
||||
npm run graphql:codegen
|
||||
```
|
||||
|
||||
Do **not** assume the existing `schema.graphql` is current after metadata changes.
|
||||
|
||||
## One-Command Setup (Reference)
|
||||
|
||||
The project includes `scripts/setup-cli.mjs` which runs this sequence in batch. Use it when the user wants a full setup; otherwise, follow this skill and run only the steps that are needed based on current state.
|
||||
|
||||
## Prohibited Actions
|
||||
|
||||
- **Do not** run `npm run graphql:schema` before metadata is deployed — the schema will not include custom objects/fields
|
||||
- **Do not** skip schema refetch after deploying new metadata — types and queries will be out of sync
|
||||
- **Do not** assign permissions or configure before deploying — permission sets, permset groups, and profiles must exist in the org first
|
||||
- **Do not** run GraphQL introspection before assigning permissions — the user may lack FLS for custom fields
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **exploring-graphql-schema** — Schema exploration (grep-only) after schema exists
|
||||
- **using-graphql** — Full GraphQL workflow (explore, query, codegen, lint)
|
||||
149
skills/exploring-webapp-graphql-schema/SKILL.md
Normal file
149
skills/exploring-webapp-graphql-schema/SKILL.md
Normal file
@ -0,0 +1,149 @@
|
||||
---
|
||||
name: exploring-webapp-graphql-schema
|
||||
description: Explore the Salesforce GraphQL schema via grep-only lookups. Use before generating any GraphQL query — schema exploration must complete first.
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce GraphQL Schema Exploration
|
||||
|
||||
Guidance for AI agents working with the Salesforce GraphQL API schema. **GREP ONLY** — the schema file is very large (~265,000+ lines). All lookups MUST use grep; do NOT open, read, stream, or parse the file.
|
||||
|
||||
## Deployment Prerequisites
|
||||
|
||||
The schema reflects the **current org state**. Custom objects and fields appear only after metadata is deployed.
|
||||
|
||||
- **Before** running `npm run graphql:schema`: Deploy all metadata (objects, permission sets, layouts) and assign the permission set to the target user. Invoke the `deploying-to-salesforce` skill for the full sequence.
|
||||
- **After** any metadata deployment: Re-run `npm run graphql:schema` and `npm run graphql:codegen` so types and queries stay in sync.
|
||||
|
||||
## Schema File Location
|
||||
|
||||
**Location:** `schema.graphql` at the **SFDX project root** (NOT inside the webapp dir). All grep commands **must be run from the project root** where `schema.graphql` lives.
|
||||
|
||||
> ⚠️ **Important (Access Policy - GREP ONLY)**: Do NOT open, view, stream, paginate, or parse the schema with any tool other than grep. All lookups MUST be done via grep using anchored patterns with minimal context as defined below.
|
||||
|
||||
If the file is not present, generate it by running (from the **webapp dir**, not the project root):
|
||||
|
||||
```bash
|
||||
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
|
||||
npm run graphql:schema
|
||||
```
|
||||
|
||||
**BEFORE generating any GraphQL query, you MUST:**
|
||||
|
||||
1. **Check if schema exists**: Look for `schema.graphql` in the **SFDX project root**
|
||||
2. **If schema is missing**:
|
||||
- `cd` to the **webapp dir** and run `npm run graphql:schema` to download it
|
||||
- Wait for the command to complete successfully
|
||||
- Then proceed with grep-only lookups as defined below.
|
||||
3. **If schema exists**: Proceed with targeted searches as described below
|
||||
|
||||
> ⚠️ **DO NOT** generate GraphQL queries without first having access to the schema. Standard field assumptions may not match the target org's configuration.
|
||||
|
||||
## Schema Structure Overview
|
||||
|
||||
Main entry points: `Query { uiapi }` for reads; `Mutation { uiapi(input: ...) }` for creates/updates/deletes. Record queries use `uiapi.query.<ObjectName>`.
|
||||
|
||||
## Allowed Lookups (grep-only)
|
||||
|
||||
Use ONLY these grep commands to locate specific definitions in schema.graphql. Do not use editors (VS Code/vim/nano), cat/less/more/head/tail, or programmatic parsers (node/python/awk/sed/jq).
|
||||
|
||||
- Always include:
|
||||
- `-n` (line numbers) and `-E` (extended regex)
|
||||
- Anchors (`^`) and word boundaries (`\b`)
|
||||
- Minimal context with `-A N` (prefer the smallest N that surfaces the needed lines)
|
||||
|
||||
### 1. Find Available Fields for a Record Type
|
||||
|
||||
Search for `type <ObjectName> implements Record` to find all queryable fields:
|
||||
|
||||
```bash
|
||||
# Example: Find Account fields (anchored, minimal context)
|
||||
grep -nE '^type[[:space:]]+Account[[:space:]]+implements[[:space:]]+Record\b' ./schema.graphql -A 60
|
||||
```
|
||||
|
||||
### 2. Find Filter Options for a Record Type
|
||||
|
||||
Search for `input <ObjectName>_Filter` to find filterable fields and operators:
|
||||
|
||||
```bash
|
||||
# Example: Find Account filter options (anchored)
|
||||
grep -nE '^input[[:space:]]+Account_Filter\b' ./schema.graphql -A 40
|
||||
```
|
||||
|
||||
### 3. Find OrderBy Options
|
||||
|
||||
Search for `input <ObjectName>_OrderBy` for sorting options:
|
||||
|
||||
```bash
|
||||
# Example: Find Account ordering options (anchored)
|
||||
grep -nE '^input[[:space:]]+Account_OrderBy\b' ./schema.graphql -A 30
|
||||
```
|
||||
|
||||
### 4. Find Mutation Operations
|
||||
|
||||
Search for operations in `UIAPIMutations`:
|
||||
|
||||
```bash
|
||||
# Example: Find Account mutations (extended regex)
|
||||
grep -nE 'Account.*(Create|Update|Delete)' ./schema.graphql
|
||||
```
|
||||
|
||||
### 5. Find Input Types for Mutations
|
||||
|
||||
Search for `input <ObjectName>CreateInput` or `input <ObjectName>UpdateInput`:
|
||||
|
||||
```bash
|
||||
# Example: Find Account create input (anchored)
|
||||
grep -nE '^input[[:space:]]+AccountCreateInput\b' ./schema.graphql -A 30
|
||||
```
|
||||
|
||||
## Agent Workflow for Building Queries (grep-only)
|
||||
|
||||
**Pre-requisites (MANDATORY):**
|
||||
|
||||
- [ ] Verified `schema.graphql` exists in the **SFDX project root**
|
||||
- [ ] If missing, ran `npm run graphql:schema` from the **webapp dir** and waited for completion
|
||||
- [ ] Confirmed connection to correct Salesforce org (if downloading fresh schema)
|
||||
|
||||
**Workflow Steps:**
|
||||
|
||||
1. **Identify the target object** (e.g., Account, Contact, Opportunity)
|
||||
2. **Run the "Find Available Fields" grep command** for your object (copy only the field names visible in the grep output; do not open the file)
|
||||
3. **Run the "Find Filter Options" grep command** (`<Object>_Filter`) to understand filtering options
|
||||
4. **Run the "Find OrderBy Options" grep command** (`<Object>_OrderBy`) for sorting capabilities
|
||||
5. **Build the query** following the patterns in the `generating-graphql-read-query` or `generating-graphql-mutation-query` skill using only values returned by grep
|
||||
6. **Validate field names** using grep matches (case-sensitive). Do not open or parse the file beyond grep.
|
||||
|
||||
## Tips for Agents
|
||||
|
||||
- **Always verify field names** by running the specific grep commands; do not open the schema file
|
||||
- **Use grep with anchors and minimal -A context** to explore the schema efficiently—never read or stream the file
|
||||
- **Check relationships** by looking for `parentRelationship` and `childRelationship` comments in type definitions
|
||||
- **Look for Connection types** (e.g., `AccountConnection`) via grep to understand pagination structure
|
||||
- **Custom objects** end with `__c` (e.g., `CustomObject__c`)
|
||||
- **Custom fields** also end with `__c` (e.g., `Custom_Field__c`)
|
||||
|
||||
## Forbidden Operations
|
||||
|
||||
To prevent accidental large reads, the following are prohibited for schema.graphql:
|
||||
|
||||
- Opening in any editor (VS Code, vim, nano)
|
||||
- Using cat, less, more, head, or tail
|
||||
- Programmatic parsing (node, python, awk, sed, jq)
|
||||
- Streaming or paginating through large portions of the file
|
||||
|
||||
If any of the above occurs, stop and replace the action with one of the Allowed Lookups (grep-only).
|
||||
|
||||
## Output Minimization
|
||||
|
||||
- Prefer precise, anchored patterns with word boundaries
|
||||
- Use the smallest `-A` context that surfaces required lines
|
||||
- If results are noisy, refine the regex rather than increasing context
|
||||
|
||||
## Related Skills
|
||||
|
||||
- For generating read queries, invoke the `generating-graphql-read-query` skill
|
||||
- For generating mutation queries, invoke the `generating-graphql-mutation-query` skill
|
||||
167
skills/fetching-webapp-rest-api/SKILL.md
Normal file
167
skills/fetching-webapp-rest-api/SKILL.md
Normal file
@ -0,0 +1,167 @@
|
||||
---
|
||||
name: fetching-webapp-rest-api
|
||||
description: REST API usage via the Data SDK fetch method. Use when implementing Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM calls — only when GraphQL is not sufficient.
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce REST API via Data SDK Fetch
|
||||
|
||||
Use `sdk.fetch` from the Data SDK when GraphQL is not sufficient. The SDK applies authentication, CSRF handling, and base URL resolution. **Always use optional chaining** (`sdk.fetch?.()`) and handle the case where `fetch` is not available.
|
||||
|
||||
Invoke this skill when you need to call Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM endpoints.
|
||||
|
||||
## API Version
|
||||
|
||||
Use the project's API version. It is typically injected as `__SF_API_VERSION__`; fallback to `"65.0"`:
|
||||
|
||||
```typescript
|
||||
declare const __SF_API_VERSION__: string;
|
||||
const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0";
|
||||
```
|
||||
|
||||
## Base Path
|
||||
|
||||
URLs are relative to the Salesforce API base. The SDK prepends the correct base path. Use paths starting with `/services/...`.
|
||||
|
||||
---
|
||||
|
||||
## Chatter API
|
||||
|
||||
User and collaboration data. No GraphQL equivalent.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/services/data/v{version}/chatter/users/me` | GET | Current user (id, name, email, username) |
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/chatter/users/me`);
|
||||
|
||||
if (!response?.ok) throw new Error(`HTTP ${response?.status}`);
|
||||
const data = await response.json();
|
||||
return { id: data.id, name: data.name };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connect REST API
|
||||
|
||||
File and content operations.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/services/data/v{version}/connect/file/upload/config` | GET | Upload config (token, uploadUrl) for file uploads |
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
const configRes = await sdk.fetch?.(`/services/data/v${API_VERSION}/connect/file/upload/config`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!configRes?.ok) throw new Error(`Failed to get upload config: ${configRes?.status}`);
|
||||
const config = await configRes.json();
|
||||
const { token, uploadUrl } = config;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Apex REST
|
||||
|
||||
Custom Apex REST resources. Requires corresponding Apex classes in the org. CSRF protection is applied automatically for `services/apexrest` URLs.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/services/apexrest/auth/login` | POST | User login |
|
||||
| `/services/apexrest/auth/register` | POST | User registration |
|
||||
| `/services/apexrest/auth/forgot-password` | POST | Request password reset |
|
||||
| `/services/apexrest/auth/reset-password` | POST | Reset password with token |
|
||||
| `/services/apexrest/auth/change-password` | POST | Change password (authenticated) |
|
||||
| `/services/apexrest/{resource}` | GET/POST | Custom Apex REST resources |
|
||||
|
||||
**Example (login):**
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.fetch?.("/services/apexrest/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password, startUrl: "/" }),
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
Apex REST paths do not include the API version.
|
||||
|
||||
---
|
||||
|
||||
## UI API (REST)
|
||||
|
||||
When GraphQL cannot cover the use case. **Prefer GraphQL** when possible.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/services/data/v{version}/ui-api/records/{recordId}` | GET | Fetch a single record |
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Einstein LLM Gateway
|
||||
|
||||
AI features. Requires Einstein API setup.
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/services/data/v{version}/einstein/llm/prompt/generations` | POST | Generate text from Einstein LLM |
|
||||
|
||||
```typescript
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
additionalConfig: { applicationName: "PromptTemplateGenerationsInvocable" },
|
||||
promptTextorId: prompt,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response?.ok) throw new Error(`Einstein LLM failed (${response?.status})`);
|
||||
const data = await response.json();
|
||||
return data?.generations?.[0]?.text ?? "";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## General Pattern
|
||||
|
||||
```typescript
|
||||
import { createDataSDK } from "@salesforce/sdk-data";
|
||||
|
||||
const sdk = await createDataSDK();
|
||||
|
||||
if (!sdk.fetch) {
|
||||
throw new Error("Data SDK fetch is not available in this context");
|
||||
}
|
||||
|
||||
const response = await sdk.fetch(url, {
|
||||
method: "GET", // or POST, PUT, PATCH, DELETE
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: method !== "GET" ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- Parent: `accessing-data` — enforces Data SDK usage for all Salesforce data fetches
|
||||
- GraphQL: `using-graphql` — use for record queries and mutations when possible
|
||||
- `createRecord` from `@salesforce/webapp-experimental/api` for UI API record creation (uses SDK internally)
|
||||
258
skills/generating-webapp-graphql-mutation-query/SKILL.md
Normal file
258
skills/generating-webapp-graphql-mutation-query/SKILL.md
Normal file
@ -0,0 +1,258 @@
|
||||
---
|
||||
name: generating-webapp-graphql-mutation-query
|
||||
description: Generate Salesforce GraphQL mutation queries. Use when the query to generate is a mutation query. Schema exploration must complete first — invoke exploring-graphql-schema first.
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce GraphQL Mutation Query Generation
|
||||
|
||||
**Triggering conditions**
|
||||
|
||||
1. Only if the schema exploration phase completed successfully (invoke `exploring-graphql-schema` first)
|
||||
2. Only if the query to generate is a mutation query
|
||||
|
||||
## Schema Access Policy
|
||||
|
||||
> ⚠️ **GREP ONLY** — During mutation generation you may need to verify field names, input types, or representations. All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep.
|
||||
|
||||
## Your Role
|
||||
|
||||
You are a GraphQL expert. Generate Salesforce-compatible mutation queries. Schema exploration must complete first. If the schema exploration has not been executed yet, you **MUST** run the full exploration workflow from the `exploring-graphql-schema` skill first, then return here for mutation query generation.
|
||||
|
||||
## Mutation Queries General Information
|
||||
|
||||
The GraphQL engine supports `Create`, `Update`, and `Delete` operations. `Update` and `Delete` operate on Id-based entity identification. See the [mutation query schema](#mutation-query-schema) section.
|
||||
|
||||
## Mutation Query Generation Workflow
|
||||
|
||||
Strictly follow the rules below when generating the GraphQL mutation query:
|
||||
|
||||
1. **Input Fields Validation** - Validate that the set of fields validate [input field constraints](#mutation-queries-input-field-constraints). Verify every field name and type against grep output from the schema — do NOT guess or assume
|
||||
2. **Output Fields Validation** - Validate that the set of fields used in the select part of the query validate the [output fields constraints](#mutation-queries-output-field-constraints)
|
||||
3. **Type Consistency** - Make sure variables used as query arguments and their related fields share the same GraphQL type. Verify types via grep lookup — do NOT assume types
|
||||
4. **Report Phase** - Use the [Mutation Query Report Template](#mutation-query-report-template) below to report on the previous validation phases
|
||||
5. **Input Arguments** - `input` is the default name for the argument, unless otherwise specified
|
||||
6. **Output Field** - For `Create` and `Update` operations, the output field is always named `Record`, and is of type EntityName
|
||||
7. **Field Name Validation** - Every field name in the generated mutation **MUST** match a field confirmed via grep lookup in the schema. Do NOT guess or assume field names exist
|
||||
8. **Query Generation** - Use the [mutation query](#mutation-query-templates) template and adjust it based on the selected operation
|
||||
9. **Output Format** - Use the [standalone](#mutation-standalone-default-output-format---clean-code-only)
|
||||
10. **Lint Validation** - After writing the mutation to a file, run `npx eslint <file>` from the webapp dir to validate it against the schema. Fix any reported errors before proceeding. See [Lint Validation](#lint-validation) for details
|
||||
11. **Test the Query** - Use the [Generated Mutation Query Testing](#generated-mutation-query-testing) workflow to test the generated query
|
||||
1. **Report First** - Always output the generated mutation in the proper output format BEFORE initiating any test
|
||||
|
||||
## Mutation Query Schema
|
||||
|
||||
**Important**: In the schema fragments below, replace **EntityName** occurrences by the real entity name (i.e. Account, Case...).
|
||||
**Important**: `Delete` operations all share the same generic `Record` entity name for both input and payload, only exposing the standard `Id` field.
|
||||
|
||||
```graphql
|
||||
input EntityNameCreateRepresentation {
|
||||
# Subset of EntityName fields here
|
||||
}
|
||||
input EntityNameCreateInput { EntityName: EntityNameCreateRepresentation! }
|
||||
type EntityNameCreatePayload { Record: EntityName! }
|
||||
|
||||
input EntityNameUpdateRepresentation {
|
||||
# Subset of EntityName fields here
|
||||
}
|
||||
input EntityNameUpdateInput { Id: IdOrRef! EntityName: EntityNameUpdateRepresentation! }
|
||||
type EntityNameUpdatePayload { Record: EntityName! }
|
||||
|
||||
input RecordDeleteInput { Id: IdOrRef! }
|
||||
type RecordDeletePayload { Id: ID }
|
||||
|
||||
type UIAPIMutations {
|
||||
EntityNameCreate(input: EntityNameCreateInput!): EntityNameCreatePayload
|
||||
EntityNameDelete(input: RecordDeleteInput!): RecordDeletePayload
|
||||
EntityNameUpdate(input: EntityNameUpdateInput!): EntityNameUpdatePayload
|
||||
}
|
||||
```
|
||||
|
||||
## Mutation Queries Input Field Constraints
|
||||
|
||||
1. **`Create` Mutation Queries**:
|
||||
1. **MUST** include all required fields
|
||||
2. **MUST** only include createable fields
|
||||
3. Child relationships can't be set and **MUST** be excluded
|
||||
4. Fields with type `REFERENCE` can only be assigned IDs through their `ApiName` name
|
||||
2. **`Update` Mutation Queries**:
|
||||
1. **MUST** include the id of the entity to update
|
||||
2. **MUST** only include updateable fields
|
||||
3. Child relationships can't be set and **MUST** be excluded
|
||||
4. Fields with type `REFERENCE` can only be assigned IDs through their `ApiName` name
|
||||
3. **`Delete` Mutation Queries**:
|
||||
1. **MUST** include the id of the entity to delete
|
||||
|
||||
## Mutation Queries Output Field Constraints
|
||||
|
||||
1. **`Create` and `Update` Mutation Queries**:
|
||||
1. **MUST** exclude all child relationships
|
||||
2. **MUST** exclude all `REFERENCE` fields, unless accessed through their `ApiName` member (no navigation to referenced entity)
|
||||
3. Inaccessible fields will be reported as part of the `errors` attribute in the returned payload
|
||||
4. Child relationships **CAN'T** be queried as part of a mutation
|
||||
5. Fields with type `REFERENCE` can only be queried through their `ApiName` (no referenced entities navigation, no sub fields)
|
||||
2. **`Delete` Mutation Queries**:
|
||||
1. **MUST** only include the `Id` field
|
||||
|
||||
## Mutation Query Report Template
|
||||
|
||||
Input arguments:
|
||||
|
||||
- Required fields: FieldName1 (Type1), FieldName2 (Type2)...
|
||||
- Other fields: FieldName3 (Type3)...
|
||||
Output fields: FieldNameA (TypeA), FieldNameB (TypeB)...
|
||||
|
||||
## Mutation Query Templates
|
||||
|
||||
```graphql
|
||||
mutation mutateEntityName(
|
||||
# arguments
|
||||
) {
|
||||
uiapi {
|
||||
EntityNameOperation(input: {
|
||||
# the following is for `Create` and `Update` operations only
|
||||
EntityName: {
|
||||
# Input fields
|
||||
}
|
||||
# the following is for `Update` and `Delete` operations only
|
||||
Id: ... # id here
|
||||
}) {
|
||||
# the following is for `Create` and `Update` operations only
|
||||
Record {
|
||||
# Output fields
|
||||
}
|
||||
# the following is for `Delete` operations only
|
||||
Id: ... # id here
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mutation Standalone (Default) Output Format - CLEAN CODE ONLY
|
||||
|
||||
```javascript
|
||||
import { gql } from '@salesforce/sdk-data';
|
||||
const QUERY_NAME = gql`
|
||||
mutation mutateEntity($input: EntityNameOperationInput!) {
|
||||
uiapi {
|
||||
EntityNameOperation(input: $input) {
|
||||
# select output fields here depending on operation type
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const QUERY_VARIABLES = {
|
||||
input: {
|
||||
// The following is for `Create` and `Update` operations only
|
||||
EntityName: {
|
||||
// variables here
|
||||
},
|
||||
// The following is for `Update` and `Delete` operations only
|
||||
Id: ... // id here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**❌ FORBIDDEN — Do NOT include any of the following:**
|
||||
|
||||
- Explanatory comments about the query (inline or surrounding)
|
||||
- Field descriptions or annotations
|
||||
- Additional text about what the query does
|
||||
- Workflow step descriptions or summaries
|
||||
- Comments like `// fetches...`, `// creates...`, `/* ... */`
|
||||
|
||||
**✅ ONLY output:**
|
||||
|
||||
- The raw query string constant (using `gql` tagged template)
|
||||
- The variables object constant
|
||||
- Nothing else — no extra imports, no exports, no wrapper functions
|
||||
|
||||
## Lint Validation
|
||||
|
||||
After writing the generated mutation into a source file, validate it against the schema using the project's GraphQL ESLint setup:
|
||||
|
||||
```bash
|
||||
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
|
||||
npx eslint <path-to-file-containing-mutation>
|
||||
```
|
||||
|
||||
**How it works:** The ESLint config uses `@graphql-eslint/eslint-plugin` with its `processor`, which extracts GraphQL operations from `gql` template literals in `.ts`/`.tsx` files and validates the extracted `.graphql` virtual files against `schema.graphql`.
|
||||
|
||||
**Rules enforced:** `no-anonymous-operations`, `no-duplicate-fields`, `known-fragment-names`, `no-undefined-variables`, `no-unused-variables`
|
||||
|
||||
**On failure:** Fix the reported issues, re-run `npx eslint <file>` until clean, then proceed to testing.
|
||||
|
||||
> ⚠️ **Prerequisites**: The `schema.graphql` file must exist (invoke `exploring-graphql-schema` first) and project dependencies must be installed (`npm install`).
|
||||
|
||||
## Generated Mutation Query Testing
|
||||
|
||||
**Triggering conditions** — **ALL conditions must be true:**
|
||||
|
||||
1. The [Mutation Query Generation Workflow](#mutation-query-generation-workflow) completed with status `SUCCESS` and you have a generated query
|
||||
2. The query is a mutation query
|
||||
3. A non-manual method was used during schema exploration to retrieve introspection data
|
||||
|
||||
**Workflow**
|
||||
|
||||
1. **Report Step** - State the exact method you will use to test (e.g., `sf api request graphql` from the **project root**, Connect API, etc.) — this **MUST** match the method used during schema exploration
|
||||
2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method
|
||||
1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user responds. Do NOT assume consent.
|
||||
3. **Input Arguments** - You **MUST** ask the user for the input argument values to use in the test
|
||||
1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user provides values. Do NOT fabricate test data.
|
||||
4. **Test Query** - Only if the user explicitly agrees and has provided input values:
|
||||
1. Execute the mutation using the reported method (e.g., `sf api request rest` to POST the query and variables to the GraphQL endpoint):
|
||||
```bash
|
||||
sf api request rest /services/data/v65.0/graphql \
|
||||
--method POST \
|
||||
--body '{"query":"mutation mutateEntity($input: EntityNameOperationInput!) { uiapi { EntityNameOperation(input: $input) { Record { Id } } } }","variables":{"input":{"EntityName":{"Field":"Value"}}}}'
|
||||
```
|
||||
2. Replace `v65.0` with the API version of the target org
|
||||
3. Replace the `query` value with the generated mutation query string
|
||||
4. Replace the `variables` value with the user-provided input arguments
|
||||
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:
|
||||
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
|
||||
2. `FAILED` if `data` is an empty object - Explanation: the query is not valid
|
||||
3. `SUCCESS` if `errors` is an empty list
|
||||
6. **Remediation Step** - If status is not `SUCCESS`, use the [`FAILED`](#failed-status-handling-workflow) or [`PARTIAL`](#partial-status-handling-workflow) status handling workflows
|
||||
|
||||
### `FAILED` Status Handling Workflow
|
||||
|
||||
The query is invalid:
|
||||
|
||||
1. **Error Analysis** - Parse and categorize the specific error messages
|
||||
2. **Root Cause Identification** - Use error message to identify the root cause:
|
||||
- **Execution** - Error contains `invalid cross reference id` or `entity is deleted`
|
||||
- **Syntax** - Error contains `invalid syntax`
|
||||
- **Validation** - Error contains `validation error`
|
||||
- **Type** - Error contains `VariableTypeMismatch` or `UnknownType`
|
||||
- **Navigation** - Error contains `is not currently available in mutation results`
|
||||
- **API Version** - Query deals with updates, you're testing with Connect API and error contains `Cannot invoke JsonElement.isJsonObject()`
|
||||
3. **Targeted Resolution** - Depending on the root cause categorization
|
||||
- **Execution** - You're trying to update or delete an unknown/no longer available entity: either create an entity first, if you have generated the related query, or ask for a valid entity id to use. **STOP and WAIT** for the user to provide a valid Id
|
||||
- **Syntax** - Update the query using the error message information to fix the syntax errors
|
||||
- **Validation** - The field name is most probably invalid. Re-run the relevant grep command from the `exploring-graphql-schema` skill to verify the correct field name. If still unclear, ask the user for clarification and **STOP and WAIT** for their answer
|
||||
- **Type** - Use the error details and re-verify the type via grep lookup in the schema. Correct the argument type and adjust variables accordingly
|
||||
- **Navigation** - Use the [`PARTIAL` status handling workflow](#partial-status-handling-workflow) below
|
||||
- **API Version** - `Record` selection is only available with API version 64 and higher, **report** the issue, and try again with API version 64
|
||||
4. **Test Again** - Resume the [query testing workflow](#generated-mutation-query-testing) with the updated query (increment and track attempt counter)
|
||||
5. **Escalation Path** - If targeted resolution fails after 2 attempts, ask for additional details and restart the entire GraphQL workflow from the `exploring-graphql-schema` skill
|
||||
|
||||
### `PARTIAL` Status Handling Workflow
|
||||
|
||||
The query can be improved:
|
||||
|
||||
1. Report the fields mentioned in the `errors` list
|
||||
2. Explain that these fields can't be queried as part of a mutation query
|
||||
3. Explain that the query might be considered as failing, as it will report errors
|
||||
4. Offer to remove the offending fields
|
||||
5. **STOP and WAIT** for the user's answer. Do NOT remove fields without explicit consent.
|
||||
6. If they are OK with removing the fields restart the [generation workflow](#mutation-query-generation-workflow) with the new field list
|
||||
|
||||
## Related Skills
|
||||
|
||||
- Schema exploration: `exploring-graphql-schema` (must complete first)
|
||||
- Read query generation: `generating-graphql-read-query`
|
||||
253
skills/generating-webapp-graphql-read-query/SKILL.md
Normal file
253
skills/generating-webapp-graphql-read-query/SKILL.md
Normal file
@ -0,0 +1,253 @@
|
||||
---
|
||||
name: generating-webapp-graphql-read-query
|
||||
description: Generate Salesforce GraphQL read queries. Use when the query to generate is a read query. Schema exploration must complete first — invoke exploring-graphql-schema first.
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce GraphQL Read Query Generation
|
||||
|
||||
**Triggering conditions**
|
||||
|
||||
1. Only if the schema exploration phase completed successfully (invoke `exploring-graphql-schema` first)
|
||||
2. Only if the query to generate is a read query
|
||||
|
||||
## Schema Access Policy
|
||||
|
||||
> ⚠️ **GREP ONLY** — During query generation you may need to verify field names, types, or relationships. All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep.
|
||||
|
||||
## Field-Level Security and @optional
|
||||
|
||||
Field-level security (FLS) restricts which fields different users can see. Use the `@optional` directive on Salesforce record fields when possible. The server omits the field when the user lacks access, allowing the query to succeed instead of failing. Apply `@optional` to scalar fields, value-type fields (e.g. `Name { value }`), parent relationships, and child relationships. Available in API v65.0+.
|
||||
|
||||
**Consuming code must defend against missing fields.** When a field is omitted due to FLS, it will be `undefined` (or absent) in the response. Use optional chaining (`?.`), nullish coalescing (`??`), and explicit null/undefined checks when reading query results. Never assume an optional field is present — otherwise the app may crash or behave incorrectly for users without field access.
|
||||
|
||||
```ts
|
||||
// ✅ Defend against missing fields
|
||||
const name = node.Name?.value ?? '';
|
||||
const relatedName = node.RelationshipName?.Name?.value ?? 'N/A';
|
||||
|
||||
// ❌ Unsafe — will throw if field omitted due to FLS
|
||||
const name = node.Name.value;
|
||||
```
|
||||
|
||||
## Your Role
|
||||
|
||||
You are a GraphQL expert. Generate Salesforce-compatible read queries. Schema exploration must complete first. If the schema exploration has not been executed yet, you **MUST** run the full exploration workflow from the `exploring-graphql-schema` skill first, then return here for read query generation.
|
||||
|
||||
## Read Query Generation Workflow
|
||||
|
||||
Strictly follow the rules below when generating the GraphQL read query:
|
||||
|
||||
1. **No Proliferation** - Only generate for the explicitly requested fields, nothing else. Do NOT add fields the user did not ask for.
|
||||
2. **Unique Query** - Leverage child relationships to query entities in one single query
|
||||
3. **Navigate Entities** - Always use `relationshipName` to access reference fields and child entities
|
||||
1. **Exception** - if the `relationshipName` field is null, you can't navigate the related entity, and will have to return the `Id` itself
|
||||
4. **Leverage Fragments** - Generate one fragment per possible type on polymorphic fields (field with `dataType="REFERENCE"` and more than one entry in `referenceToInfos` introspection attribute)
|
||||
5. **Type Consistency** - Make sure variables used as query arguments and their related fields share the same GraphQL type. Verify types against grep output from the schema — do not assume types
|
||||
6. **Type Enforcement** - Make sure to leverage field type information from introspection and GraphQL schema to generate field access
|
||||
7. **Field Name Validation** - Every field name in the generated query **MUST** match a field confirmed via grep lookup in the schema. Do NOT guess or assume field names exist
|
||||
8. **@optional for FLS** - Apply `@optional` on all Salesforce record fields when possible (see [Field-Level Security and @optional](#field-level-security-and-optional)). This lets the query succeed when the user lacks field-level access; the server omits inaccessible fields instead of failing
|
||||
9. **Consuming code defense** - When generating or modifying code that consumes read query results, defend against missing fields (see [Field-Level Security and @optional](#field-level-security-and-optional)). Use optional chaining, nullish coalescing, and null/undefined checks — never assume optional fields are present
|
||||
10. **Semi and anti joins** - Use the semi-join or anti-join templates to filter an entity with conditions on child entities
|
||||
11. **Query Generation** - Use the [template](#read-query-template) to generate the query
|
||||
12. **Output Format** - Use the [standalone](#read-standalone-default-output-format---clean-code-only)
|
||||
13. **Lint Validation** - After writing the query to a file, run `npx eslint <file>` from the webapp dir to validate it against the schema. Fix any reported errors before proceeding. See [Lint Validation](#lint-validation) for details
|
||||
14. **Test the Query** - Use the [Generated Read Query Testing](#generated-read-query-testing) workflow to test the generated query
|
||||
1. **Report First** - Always output the generated query in the proper output format BEFORE initiating any test
|
||||
|
||||
## Read Query Template
|
||||
|
||||
```graphql
|
||||
query QueryName {
|
||||
uiapi {
|
||||
query {
|
||||
EntityName(
|
||||
# conditions here
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
# Direct fields — use @optional for FLS resilience
|
||||
FieldName @optional { value }
|
||||
|
||||
# Non-polymorphic reference (single type)
|
||||
RelationshipName @optional {
|
||||
Id
|
||||
Name { value }
|
||||
}
|
||||
|
||||
# Polymorphic reference (multiple types)
|
||||
PolymorphicRelationshipName @optional {
|
||||
...TypeAInfo
|
||||
...TypeBInfo
|
||||
}
|
||||
|
||||
# Child relationship (subquery)
|
||||
RelationshipName @optional (
|
||||
# conditions here
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
# fields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment TypeAInfo on TypeA {
|
||||
Id
|
||||
SpecificFieldA @optional { value }
|
||||
}
|
||||
|
||||
fragment TypeBInfo on TypeB {
|
||||
Id
|
||||
SpecificFieldB @optional { value }
|
||||
}
|
||||
```
|
||||
|
||||
## Semi-Join and Anti-Join Condition Template
|
||||
|
||||
Semi-joins (resp. anti-joins) condition leverage parent-child relationships and allow filtering the parent entity using a condition on child entities.
|
||||
This is a standard `where` condition, on the parent entity's `Id`, expressed using the `inq` (resp. `ninq`, i.e. not `inq`) operator. This operator accepts two attributes:
|
||||
|
||||
- The child entity camelcase name to apply the condition on, with a value expressing the condition
|
||||
- The field name on the child entity containing the parent entity `Id`, which is the `fieldName` from the `childRelationships` information for the child entity
|
||||
- If the only condition is related child entity existence, you can use an `Id: { ne: null }` condition
|
||||
|
||||
### Semi-Join Example - ParentEntity with at least one Matching ChildEntity
|
||||
|
||||
```graphql
|
||||
query testSemiJoin {
|
||||
uiapi {
|
||||
query {
|
||||
ParentEntity(
|
||||
where: {
|
||||
Id: {
|
||||
inq: {
|
||||
ChildEntity: {
|
||||
# standard conditions here
|
||||
Name: { like: "test%" }
|
||||
Type: { eq: "some value" }
|
||||
}
|
||||
ApiName: "parentIdFieldInChild"
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
Name @optional {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Join Example - ParentEntity with no Matching ChildEntity
|
||||
|
||||
Same example as the [Semi-Join Example](#semi-join-example---parententity-with-at-least-one-matching-childentity), but replacing the `inq` operator by the `ninq` one.
|
||||
|
||||
## Read Standalone (Default) Output Format - CLEAN CODE ONLY
|
||||
|
||||
```javascript
|
||||
const QUERY_NAME = `
|
||||
query GetData {
|
||||
# query here
|
||||
}
|
||||
`;
|
||||
|
||||
const QUERY_VARIABLES = {
|
||||
// variables here
|
||||
};
|
||||
```
|
||||
|
||||
**❌ FORBIDDEN — Do NOT include any of the following:**
|
||||
|
||||
- Explanatory comments about the query (inline or surrounding)
|
||||
- Field descriptions or annotations
|
||||
- Additional text about what the query does
|
||||
- Workflow step descriptions or summaries
|
||||
- Comments like `// fetches...`, `// returns...`, `/* ... */`
|
||||
|
||||
**✅ ONLY output:**
|
||||
|
||||
- The raw query string constant
|
||||
- The variables object constant
|
||||
- Nothing else — no imports, no exports, no wrapper functions
|
||||
|
||||
## Lint Validation
|
||||
|
||||
After writing the generated query into a source file, validate it against the schema using the project's GraphQL ESLint setup:
|
||||
|
||||
```bash
|
||||
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
|
||||
npx eslint <path-to-file-containing-query>
|
||||
```
|
||||
|
||||
**How it works:** The ESLint config uses `@graphql-eslint/eslint-plugin` with its `processor`, which extracts GraphQL operations from `gql` template literals in `.ts`/`.tsx` files and validates the extracted `.graphql` virtual files against `schema.graphql`.
|
||||
|
||||
**Rules enforced:** `no-anonymous-operations`, `no-duplicate-fields`, `known-fragment-names`, `no-undefined-variables`, `no-unused-variables`
|
||||
|
||||
**On failure:** Fix the reported issues, re-run `npx eslint <file>` until clean, then proceed to testing.
|
||||
|
||||
> ⚠️ **Prerequisites**: The `schema.graphql` file must exist (invoke `exploring-graphql-schema` first) and project dependencies must be installed (`npm install`).
|
||||
|
||||
## Generated Read Query Testing
|
||||
|
||||
**Triggering conditions** — **ALL conditions must be true:**
|
||||
|
||||
1. The [Read Query Generation Workflow](#read-query-generation-workflow) completed with status `SUCCESS` and you have a generated query
|
||||
2. The query is a read query
|
||||
3. A non-manual method was used during schema exploration to retrieve introspection data
|
||||
|
||||
**Workflow**
|
||||
|
||||
1. **Report Step** - State the exact method you will use to test (e.g., `sf api request graphql` from the **project root**, Connect API, etc.) — this **MUST** match the method used during schema exploration
|
||||
2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method
|
||||
1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user responds. Do NOT assume consent.
|
||||
3. **Test Query** - Only if the user explicitly agrees:
|
||||
1. Use `sf api request rest` to POST the query to the GraphQL endpoint:
|
||||
```bash
|
||||
sf api request rest /services/data/v65.0/graphql \
|
||||
--method POST \
|
||||
--body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}'
|
||||
```
|
||||
2. Replace `v65.0` with the API version of the target org
|
||||
3. Replace the `query` value with the generated read query string
|
||||
4. If the query uses variables, include them in the JSON body as a `variables` key
|
||||
5. Report the result as `SUCCESS` if the query executed without error, or `FAILED` if errors were returned
|
||||
6. An empty result set with no errors is `SUCCESS` — the query is valid, the org simply has no matching data
|
||||
4. **Remediation Step** - If status is `FAILED`, use the [`FAILED` status handling workflows](#failed-status-handling-workflow)
|
||||
|
||||
### `FAILED` Status Handling Workflow
|
||||
|
||||
The query is invalid:
|
||||
|
||||
1. **Error Analysis** - Parse and categorize the specific error messages
|
||||
2. **Root Cause Identification** - Use error message to identify the root cause:
|
||||
- **Syntax** - Error contains `invalid syntax`
|
||||
- **Validation** - Error contains `validation error`
|
||||
- **Type** - Error contains `VariableTypeMismatch` or `UnknownType`
|
||||
3. **Targeted Resolution** - Depending on the root cause categorization
|
||||
- **Syntax** - Update the query using the error message information to fix the syntax errors
|
||||
- **Validation** - The field name is most probably invalid. Re-run the relevant grep command from the `exploring-graphql-schema` skill to verify the correct field name. If still unclear, ask the user for clarification and **STOP and WAIT** for their answer
|
||||
- **Type** - Use the error details and re-verify the type via grep lookup in the schema. Correct the argument type and adjust variables accordingly
|
||||
4. **Test Again** - Resume the [query testing workflow](#generated-read-query-testing) with the updated query (increment and track attempt counter)
|
||||
5. **Escalation Path** - If targeted resolution fails after 2 attempts, ask for additional details and restart the entire GraphQL workflow from the `exploring-graphql-schema` skill
|
||||
|
||||
## Related Skills
|
||||
|
||||
- Schema exploration: `exploring-graphql-schema` (must complete first)
|
||||
- Mutation generation: `generating-graphql-mutation-query`
|
||||
396
skills/implementing-webapp-file-upload/SKILL.md
Normal file
396
skills/implementing-webapp-file-upload/SKILL.md
Normal file
@ -0,0 +1,396 @@
|
||||
---
|
||||
name: implementing-webapp-file-upload
|
||||
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.
|
||||
---
|
||||
|
||||
# File Upload API (workflow)
|
||||
|
||||
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.
|
||||
|
||||
## CRITICAL: This is an API-only package
|
||||
|
||||
The package exports **programmatic APIs**, not React components or hooks. You will:
|
||||
|
||||
- Use the `upload()` function to handle file uploads with progress tracking
|
||||
- Build your own custom UI (file input, dropzone, progress bars, etc.)
|
||||
- Track upload progress through the `onProgress` callback
|
||||
|
||||
**Do NOT:**
|
||||
|
||||
- Expect pre-built components like `<FileUpload />` — they are not exported
|
||||
- Try to import React hooks like `useFileUpload` — they are not exported
|
||||
- Look for dropzone components — they are not exported
|
||||
|
||||
The source code contains reference components for demonstration, but they are **not available** as imports. Use them as examples to build your own UI.
|
||||
|
||||
## 1. Install the package
|
||||
|
||||
```bash
|
||||
npm install @salesforce/webapp-template-feature-react-file-upload-experimental
|
||||
```
|
||||
|
||||
Dependencies are automatically installed:
|
||||
|
||||
- `@salesforce/webapp-experimental` (API client)
|
||||
- `@salesforce/sdk-data` (data SDK)
|
||||
|
||||
## 2. Understand the three upload patterns
|
||||
|
||||
### Pattern A: Basic upload (no record linking)
|
||||
|
||||
Upload files to Salesforce and get back `contentBodyId` for each file. No ContentVersion record is created.
|
||||
|
||||
**When to use:**
|
||||
|
||||
- User wants to upload files first, then create/link them to a record later
|
||||
- Building a multi-step form where the record doesn't exist yet
|
||||
- Deferred record linking scenarios
|
||||
|
||||
```tsx
|
||||
import { upload } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
|
||||
|
||||
const results = await upload({
|
||||
files: [file1, file2],
|
||||
onProgress: (progress) => {
|
||||
console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
|
||||
},
|
||||
});
|
||||
|
||||
// results[0].contentBodyId: "069..." (always available)
|
||||
// results[0].contentVersionId: undefined (no record linked)
|
||||
```
|
||||
|
||||
### Pattern B: Upload with immediate record linking
|
||||
|
||||
Upload files and immediately link them to an existing Salesforce record by creating ContentVersion records.
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Record already exists (Account, Opportunity, Case, etc.)
|
||||
- User wants files immediately attached to the record
|
||||
- Direct upload-and-attach scenarios
|
||||
|
||||
```tsx
|
||||
import { upload } from "@salesforce/webapp-template-feature-react-file-upload-experimental";
|
||||
|
||||
const results = await upload({
|
||||
files: [file1, file2],
|
||||
recordId: "001xx000000yyyy", // Existing record ID
|
||||
onProgress: (progress) => {
|
||||
console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
|
||||
},
|
||||
});
|
||||
|
||||
// results[0].contentBodyId: "069..." (always available)
|
||||
// results[0].contentVersionId: "068..." (linked to record)
|
||||
```
|
||||
|
||||
### Pattern C: Deferred record linking (record creation flow)
|
||||
|
||||
Upload files without a record, then link them after the record is created.
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Building a "create record with attachments" form
|
||||
- Record doesn't exist until form submission
|
||||
- Need to upload files before knowing the final record ID
|
||||
|
||||
```tsx
|
||||
import {
|
||||
upload,
|
||||
createContentVersion,
|
||||
} from "@salesforce/webapp-template-feature-react-file-upload-experimental";
|
||||
|
||||
// Step 1: Upload files (no recordId)
|
||||
const uploadResults = await upload({
|
||||
files: [file1, file2],
|
||||
onProgress: (progress) => console.log(progress),
|
||||
});
|
||||
|
||||
// Step 2: Create the record
|
||||
const newRecordId = await createRecord(formData);
|
||||
|
||||
// Step 3: Link uploaded files to the new record
|
||||
for (const file of uploadResults) {
|
||||
const contentVersionId = await createContentVersion(
|
||||
new File([""], file.fileName),
|
||||
file.contentBodyId,
|
||||
newRecordId,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Build your custom UI
|
||||
|
||||
The package provides the backend — you build the frontend. Here's a minimal example:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
upload,
|
||||
type FileUploadProgress,
|
||||
} from "@salesforce/webapp-template-feature-react-file-upload-experimental";
|
||||
import { useState } from "react";
|
||||
|
||||
function CustomFileUpload({ recordId }: { recordId?: string }) {
|
||||
const [progress, setProgress] = useState<Map<string, FileUploadProgress>>(new Map());
|
||||
|
||||
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
|
||||
await upload({
|
||||
files,
|
||||
recordId,
|
||||
onProgress: (fileProgress) => {
|
||||
setProgress((prev) => new Map(prev).set(fileProgress.fileName, fileProgress));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="file" multiple onChange={handleFileSelect} />
|
||||
|
||||
{Array.from(progress.entries()).map(([fileName, fileProgress]) => (
|
||||
<div key={fileName}>
|
||||
{fileName}: {fileProgress.status} - {fileProgress.progress}%
|
||||
{fileProgress.error && <span>Error: {fileProgress.error}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Track upload progress
|
||||
|
||||
The `onProgress` callback fires multiple times for each file as it moves through stages:
|
||||
|
||||
| Status | When | Progress Value |
|
||||
| -------------- | ---------------------------------------------- | -------------------- |
|
||||
| `"pending"` | File queued for upload | `0` |
|
||||
| `"uploading"` | Upload in progress (XHR) | `0-100` (percentage) |
|
||||
| `"processing"` | Creating ContentVersion (if recordId provided) | `0` |
|
||||
| `"success"` | Upload complete | `100` |
|
||||
| `"error"` | Upload failed | `0` |
|
||||
|
||||
**Always provide visual feedback:**
|
||||
|
||||
- Show file name
|
||||
- Display current status
|
||||
- Render progress bar for "uploading" status
|
||||
- Show error message if status is "error"
|
||||
|
||||
## 5. Cancel uploads (optional)
|
||||
|
||||
Use an `AbortController` to allow users to cancel uploads:
|
||||
|
||||
```tsx
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handleUpload = async (files: File[]) => {
|
||||
try {
|
||||
await upload({
|
||||
files,
|
||||
signal: abortController.signal,
|
||||
onProgress: (progress) => console.log(progress),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Upload cancelled or failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelUpload = () => {
|
||||
abortController.abort();
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Link to current user (special case)
|
||||
|
||||
If the user wants to upload files to their own profile or personal library:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
upload,
|
||||
getCurrentUserId,
|
||||
} from "@salesforce/webapp-template-feature-react-file-upload-experimental";
|
||||
|
||||
const userId = await getCurrentUserId();
|
||||
await upload({ files, recordId: userId });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### upload(options)
|
||||
|
||||
Main upload API that handles complete flow with progress tracking.
|
||||
|
||||
```typescript
|
||||
interface UploadOptions {
|
||||
files: File[];
|
||||
recordId?: string | null; // If provided, creates ContentVersion
|
||||
onProgress?: (progress: FileUploadProgress) => void;
|
||||
signal?: AbortSignal; // Optional cancellation
|
||||
}
|
||||
|
||||
interface FileUploadProgress {
|
||||
fileName: string;
|
||||
status: "pending" | "uploading" | "processing" | "success" | "error";
|
||||
progress: number; // 0-100 for uploading, 0 for other states
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface FileUploadResult {
|
||||
fileName: string;
|
||||
size: number;
|
||||
contentBodyId: string; // Always available
|
||||
contentVersionId?: string; // Only if recordId was provided
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:** `Promise<FileUploadResult[]>`
|
||||
|
||||
### createContentVersion(file, contentBodyId, recordId)
|
||||
|
||||
Manually create a ContentVersion record from a previously uploaded file.
|
||||
|
||||
```typescript
|
||||
async function createContentVersion(
|
||||
file: File,
|
||||
contentBodyId: string,
|
||||
recordId: string,
|
||||
): Promise<string | undefined>;
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `file` — File object (used for metadata like name)
|
||||
- `contentBodyId` — ContentBody ID from previous upload
|
||||
- `recordId` — Record ID for FirstPublishLocationId
|
||||
|
||||
**Returns:** ContentVersion ID if successful
|
||||
|
||||
### getCurrentUserId()
|
||||
|
||||
Get the current user's Salesforce ID.
|
||||
|
||||
```typescript
|
||||
async function getCurrentUserId(): Promise<string>;
|
||||
```
|
||||
|
||||
**Returns:** Current user ID
|
||||
|
||||
## Common UI patterns
|
||||
|
||||
### File input with button
|
||||
|
||||
```tsx
|
||||
<input type="file" multiple accept=".pdf,.doc,.docx,.jpg,.png" onChange={handleFileSelect} />
|
||||
```
|
||||
|
||||
### Drag-and-drop zone
|
||||
|
||||
Build your own dropzone using native events:
|
||||
|
||||
```tsx
|
||||
function DropZone({ onDrop }: { onDrop: (files: File[]) => void }) {
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
onDrop(files);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
style={{ border: "2px dashed #ccc", padding: "2rem" }}
|
||||
>
|
||||
Drop files here
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Progress bar
|
||||
|
||||
```tsx
|
||||
{
|
||||
progress.status === "uploading" && (
|
||||
<div style={{ width: "100%", background: "#eee" }}>
|
||||
<div
|
||||
style={{
|
||||
width: `${progress.progress}%`,
|
||||
background: "#0176d3",
|
||||
height: "8px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Decision tree for agents
|
||||
|
||||
**User asks for file upload functionality:**
|
||||
|
||||
1. **Ask about record context:**
|
||||
- "Do you want to link uploaded files to a specific record, or upload them first and link later?"
|
||||
|
||||
2. **Based on response:**
|
||||
- **Link to existing record** → Use Pattern B with `recordId`
|
||||
- **Upload first, link later** → Use Pattern A (no recordId), then Pattern C for linking
|
||||
- **Link to current user** → Use Pattern B with `getCurrentUserId()`
|
||||
|
||||
3. **Build the UI:**
|
||||
- Create file input or dropzone (not provided by package)
|
||||
- Add progress display for each file (status + progress bar)
|
||||
- Handle errors in the UI
|
||||
|
||||
4. **Test the implementation:**
|
||||
- Verify progress callbacks fire correctly
|
||||
- Check that `contentBodyId` is returned
|
||||
- If `recordId` was provided, verify `contentVersionId` is returned
|
||||
|
||||
## Reference implementation
|
||||
|
||||
The package includes a reference implementation in `src/features/fileupload/` with:
|
||||
|
||||
- `FileUpload.tsx` — Complete component with dropzone and dialog
|
||||
- `FileUploadDialog.tsx` — Progress tracking dialog
|
||||
- `FileUploadDropZone.tsx` — Drag-and-drop zone
|
||||
- `useFileUpload.ts` — React hook for state management
|
||||
|
||||
**These are NOT exported** but can be viewed as examples. Read the source files to understand patterns for building your own UI.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Upload fails with CORS error:**
|
||||
|
||||
- Ensure the webapp is properly deployed to Salesforce or running on `localhost`
|
||||
- Check that the org allows the origin in CORS settings
|
||||
|
||||
**No progress updates:**
|
||||
|
||||
- Verify `onProgress` callback is provided
|
||||
- Check that the callback function updates React state correctly
|
||||
|
||||
**ContentVersion not created:**
|
||||
|
||||
- Verify `recordId` is provided to `upload()` function
|
||||
- Check that the record ID is valid and exists in the org
|
||||
- Ensure user has permissions to create ContentVersion records
|
||||
|
||||
**Files upload but don't appear in record:**
|
||||
|
||||
- Verify `recordId` is correct
|
||||
- Check that ContentVersion was created (look for `contentVersionId` in results)
|
||||
- Confirm user has access to view files on the record
|
||||
|
||||
## DO NOT do these things
|
||||
|
||||
- ❌ Build XHR/fetch upload logic from scratch — use the `upload()` API
|
||||
- ❌ Try to import `<FileUpload />` component — it's not exported
|
||||
- ❌ Try to import `useFileUpload` hook — it's not exported
|
||||
- ❌ Use third-party file upload libraries when this feature exists
|
||||
- ❌ Skip progress tracking — always provide user feedback
|
||||
- ❌ Ignore errors — always handle and display error messages
|
||||
210
skills/installing-webapp-features/SKILL.md
Normal file
210
skills/installing-webapp-features/SKILL.md
Normal file
@ -0,0 +1,210 @@
|
||||
---
|
||||
name: installing-webapp-features
|
||||
description: Search, describe, and install pre-built UI features (authentication, shadcn components, navigation, search, GraphQL, Agentforce AI) into Salesforce webapps. Use this when the user wants to add functionality to a webapp, or when determining what salesforce-provided features are available — whether prompted by the user or on your own initiative. Always check for an existing feature before building from scratch.
|
||||
---
|
||||
|
||||
# webapps-features-experimental CLI — Agent Reference
|
||||
|
||||
**Always check for an existing feature before building something yourself.** This CLI installs pre-built, tested feature packages into Salesforce webapps. Features range from foundational UI component libraries (shadcn/ui with Button, Card, Input, Table, etc.) to full-stack application capabilities like authentication (login, registration, password flows, session management, and Apex backend classes), global search, navigation menus, GraphQL integrations, and Agentforce AI conversation UIs. Each feature ships as a complete implementation — including React components, context providers, route guards, and any required Salesforce server-side code — that already handles platform-specific concerns like Salesforce API integration, session management, and SFDX metadata structure. Building these from scratch is error-prone and unnecessary when a feature exists. **If no existing feature is found, ask the user before proceeding with a custom implementation — a relevant feature may exist under a different name or keyword.**
|
||||
|
||||
```
|
||||
npx @salesforce/webapps-features-experimental <command> [options]
|
||||
```
|
||||
|
||||
## Workflow: Search Project → Search Features → Describe → Install
|
||||
|
||||
**MANDATORY**: When the user asks to add ANY webapp functionality, follow this entire workflow. Do not skip steps.
|
||||
|
||||
### 1. Search existing project code
|
||||
|
||||
Before installing anything, check whether the functionality already exists in the **project source code** (not dependencies).
|
||||
|
||||
- **Always scope searches to `src/`** to avoid matching files in `node_modules/`, `dist/`, or `build/` output
|
||||
- Use Glob with a scoped path: e.g., `src/**/Button.tsx`, `src/**/*auth*.tsx`
|
||||
- Use Grep with the `path` parameter set to the `src/` directory, or use `glob: "*.{ts,tsx}"` to restrict file types
|
||||
- Check common directories: `src/components/`, `src/lib/`, `src/pages/`, `src/hooks/`
|
||||
- **Never** search from the project root without a path or glob filter — this will crawl `node_modules` and produce massive, unhelpful output
|
||||
|
||||
**If existing code is found** — read the files, present them to the user, and ask if they want to reuse or extend what's there. If yes, use the existing code and stop. If no, proceed to step 2.
|
||||
|
||||
**If nothing is found** — proceed to step 2.
|
||||
|
||||
### 2. Search available features
|
||||
|
||||
```bash
|
||||
npx @salesforce/webapps-features-experimental list [options]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `-v, --verbose` — Show full descriptions, packages, and dependencies
|
||||
- `--search <query>` — Filter features by keyword (ranked by relevance)
|
||||
|
||||
```bash
|
||||
npx @salesforce/webapps-features-experimental list
|
||||
npx @salesforce/webapps-features-experimental list --search "auth"
|
||||
npx @salesforce/webapps-features-experimental list --search "button"
|
||||
```
|
||||
|
||||
**If no matching feature is found** — ask the user before proceeding with a custom implementation. A relevant feature may exist under a different name or keyword.
|
||||
|
||||
### 3. Describe a feature
|
||||
|
||||
```bash
|
||||
npx @salesforce/webapps-features-experimental describe <feature>
|
||||
```
|
||||
|
||||
Shows description, package name, dependencies, components, copy operations, and example files.
|
||||
|
||||
```bash
|
||||
npx @salesforce/webapps-features-experimental describe authentication
|
||||
npx @salesforce/webapps-features-experimental describe shadcn
|
||||
```
|
||||
|
||||
### 4. Install a feature
|
||||
|
||||
```bash
|
||||
npx @salesforce/webapps-features-experimental install <feature> --webapp-dir <path> [options]
|
||||
```
|
||||
|
||||
Resolves the feature name to an npm package, installs it and its dependencies (including transitive feature dependencies like `shadcn`), copies source files into your project, and reports any `__example__` files that require manual integration.
|
||||
|
||||
Options:
|
||||
|
||||
- `--webapp-dir <name>` (required) — Webapp name, resolves to `<sfdx-source>/webapplications/<name>`
|
||||
- `--sfdx-source <path>` (default: `force-app/main/default`) — SFDX source directory
|
||||
- `--dry-run` (default: `false`) — Preview changes without writing files
|
||||
- `-v, --verbose` (default: `false`) — Enable verbose logging
|
||||
- `-y, --yes` (default: `false`) — Skip all prompts (auto-skip conflicts)
|
||||
- `--on-conflict <mode>` (default: `prompt`) — `prompt`, `error`, `skip`, or `overwrite`
|
||||
- `--conflict-resolution <file>` — Path to JSON file with per-file resolutions
|
||||
|
||||
```bash
|
||||
# Install authentication (also installs shadcn dependency)
|
||||
npx @salesforce/webapps-features-experimental install authentication \
|
||||
--webapp-dir mywebapp
|
||||
|
||||
# Dry run to preview changes
|
||||
npx @salesforce/webapps-features-experimental install shadcn \
|
||||
--webapp-dir mywebapp \
|
||||
--dry-run
|
||||
|
||||
# Non-interactive install (skip all file conflicts)
|
||||
npx @salesforce/webapps-features-experimental install authentication \
|
||||
--webapp-dir mywebapp \
|
||||
--yes
|
||||
```
|
||||
|
||||
## Conflict Handling
|
||||
|
||||
Since you are running in a non-interactive environment, you cannot use `--on-conflict prompt` directly. When conflicts are likely (e.g. installing into an existing project), you have two options:
|
||||
|
||||
**Option A — Let the user resolve conflicts interactively.** Suggest the user run the install command themselves with `--on-conflict prompt` so they can decide per-file.
|
||||
|
||||
**Option B — Two-pass automated resolution:**
|
||||
|
||||
```bash
|
||||
# Pass 1: detect conflicts
|
||||
npx @salesforce/webapps-features-experimental install authentication \
|
||||
--webapp-dir mywebapp \
|
||||
--on-conflict error
|
||||
|
||||
# The CLI will exit with an error listing every conflicting file path.
|
||||
|
||||
# Pass 2: create a resolution file and re-run
|
||||
echo '{ "src/styles/global.css": "overwrite", "src/lib/utils.ts": "skip" }' > resolutions.json
|
||||
|
||||
npx @salesforce/webapps-features-experimental install authentication \
|
||||
--webapp-dir mywebapp \
|
||||
--conflict-resolution resolutions.json
|
||||
```
|
||||
|
||||
Resolution values per file: `"skip"` (keep existing) or `"overwrite"` (replace). When unsure how to resolve a conflict, ask the user rather than guessing.
|
||||
|
||||
## Hint Placeholders in Copy Paths
|
||||
|
||||
Some copy operations use **hint placeholders** in the `"to"` path — descriptive segments like `<desired-page-with-search-input>` that are NOT resolved by the CLI. These are guidance for the user or LLM to choose an appropriate destination.
|
||||
|
||||
**How they work:** The file is copied with the literal placeholder name (e.g., `src/pages/<desired-page-with-search-input>.tsx`). After installation, you should:
|
||||
|
||||
1. Read the copied file to understand its purpose
|
||||
2. Rename or relocate it to the intended target (e.g., `src/pages/Home.tsx`)
|
||||
3. Or integrate its patterns into an existing file, then delete it
|
||||
|
||||
**How to identify them:** Hint placeholders use `<descriptive-name>` syntax but are NOT one of the system placeholders (`<sfdxSource>`, `<webappDir>`, `<webapp>`). They always appear in the middle or end of a path, never as the leading segment.
|
||||
|
||||
**Example from features.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"to": "<webappDir>/src/pages/<desired-page-with-search-input>.tsx",
|
||||
"description": "Example home page showing GlobalSearchInput integration",
|
||||
"integrationTarget": "src/pages/Home.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
The `integrationTarget` field tells you the suggested destination. Use your judgment — if the user already has a different page where search should go, integrate there instead.
|
||||
|
||||
**When `integrationTarget` itself is a placeholder:** Some features use a hint placeholder in the `integrationTarget` value (e.g., `"integrationTarget": "src/<path-to-desired-page-with-search-input>.tsx"`). This means there is no single default target — the user must decide which existing file to integrate into. When you encounter this:
|
||||
|
||||
1. Ask the user which page or file they want to integrate the feature into
|
||||
2. Read the `__example__` file to understand the integration pattern
|
||||
3. Read the user's chosen target file
|
||||
4. Apply the pattern from the example into the target file
|
||||
|
||||
## Post Installation: Integrating **example** Files
|
||||
|
||||
Features may include `__example__` files (e.g., `__example__auth-app.tsx`) showing integration patterns.
|
||||
|
||||
**The describe command shows**:
|
||||
|
||||
- Which **example** files will be copied
|
||||
- Target file to integrate into (e.g., `src/app.tsx`)
|
||||
- What the example demonstrates
|
||||
|
||||
### How to Integrate Example Files (CRITICAL FOR LLMs)
|
||||
|
||||
⚠️ **ONLY USE Read AND Edit TOOLS - NO BASH COMMANDS** ⚠️
|
||||
|
||||
**DO NOT DO THIS**:
|
||||
|
||||
- ❌ `git status` or any git commands
|
||||
- ❌ `ls`, `cat`, `sed`, `awk`, or ANY bash file commands
|
||||
- ❌ Chaining bash commands to read multiple files
|
||||
- ❌ Using bash to check directories or file existence
|
||||
|
||||
**DO THIS INSTEAD**:
|
||||
|
||||
- ✅ Use Read tool with `file_path` parameter to read each file
|
||||
- ✅ Use Edit tool with `file_path`, `old_string`, `new_string` to modify files
|
||||
- ✅ That's it! Just Read and Edit tools.
|
||||
|
||||
**Integration steps**:
|
||||
|
||||
1. **Read each example file** (use Read tool)
|
||||
- Example: Read tool with `file_path: "force-app/main/default/webapplications/mywebapp/src/__example__auth-app.tsx"`
|
||||
- Note the imports and patterns to integrate
|
||||
|
||||
2. **Read each target file** (use Read tool)
|
||||
- Example: Read tool with `file_path: "force-app/main/default/webapplications/mywebapp/src/app.tsx"`
|
||||
- Understand where the new code should go
|
||||
|
||||
3. **Edit each target file** (use Edit tool)
|
||||
- Add imports from the example
|
||||
- Add or modify code following the example's patterns
|
||||
- Preserve existing functionality
|
||||
|
||||
4. **Delete the example file after successful integration** (use Bash tool)
|
||||
- Example: `rm force-app/main/default/webapplications/mywebapp/src/__example__authentication-routes.tsx`
|
||||
- Only delete after you have successfully integrated the pattern
|
||||
- This keeps the codebase clean and removes temporary example files
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Directory not found**: Check paths are correct, use absolute or correct relative paths
|
||||
|
||||
**Feature not found**: Use `npx @salesforce/webapps-features-experimental list` to see available feature names
|
||||
|
||||
**Conflicts in error mode**: Follow CLI instructions to create resolution file
|
||||
|
||||
**Need help?**: Run `npx @salesforce/webapps-features-experimental --help` to see all commands and options
|
||||
186
skills/managing-webapp-agentforce-conversation-client/SKILL.md
Normal file
186
skills/managing-webapp-agentforce-conversation-client/SKILL.md
Normal file
@ -0,0 +1,186 @@
|
||||
---
|
||||
name: managing-webapp-agentforce-conversation-client
|
||||
description: Adds or modifies AgentforceConversationClient in React apps (.tsx or .jsx files). Use when user says "add chat widget", "embed agentforce", "add agent", "add chatbot", "integrate conversational AI", or asks to change colors, dimensions, styling, or configure agentId, width, height, inline mode, or styleTokens for travel agent, HR agent, employee agent, or any Salesforce agent chat.
|
||||
metadata:
|
||||
author: ACC Components
|
||||
version: 1.0.0
|
||||
package: "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental"
|
||||
sdk-package: "@salesforce/agentforce-conversation-client"
|
||||
last-updated: 2025-03-18
|
||||
---
|
||||
|
||||
# Managing Agentforce Conversation Client
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1: Check if component already exists
|
||||
|
||||
Search for existing usage across all app files (not implementation files):
|
||||
|
||||
```bash
|
||||
grep -r "AgentforceConversationClient" --include="*.tsx" --include="*.jsx" --exclude-dir=node_modules
|
||||
```
|
||||
|
||||
**Important:** Look for React files that import and USE the component (for example, shared shells, route components, or feature pages). Do NOT open files named `AgentforceConversationClient.tsx` or `AgentforceConversationClient.jsx` - those are the component implementation.
|
||||
|
||||
**If found:** Read the file and check the current `agentId` value.
|
||||
|
||||
**Agent ID validation rule (deterministic):**
|
||||
|
||||
- Valid only if it matches: `^0Xx[a-zA-Z0-9]{15}$`
|
||||
- Meaning: starts with `0Xx` and total length is 18 characters
|
||||
|
||||
**Decision:**
|
||||
|
||||
- If `agentId` matches `^0Xx[a-zA-Z0-9]{15}$` and user wants to update other props → Go to Step 4 (update props)
|
||||
- If `agentId` is missing, empty, or does NOT match `^0Xx[a-zA-Z0-9]{15}$` → Continue to Step 2 (need real ID)
|
||||
- If not found → Continue to Step 2 (add new)
|
||||
|
||||
### Step 2: Get agent ID
|
||||
|
||||
If component doesn't exist or has an invalid placeholder value, ask user for their Salesforce agent ID.
|
||||
|
||||
Treat these as placeholder/invalid values:
|
||||
|
||||
- `"0Xx..."`
|
||||
- `"Placeholder"`
|
||||
- `"YOUR_AGENT_ID"`
|
||||
- `"<USER_AGENT_ID_18_CHAR_0Xx...>"`
|
||||
- Any value that does not match `^0Xx[a-zA-Z0-9]{15}$`
|
||||
|
||||
Skip this step if:
|
||||
|
||||
- Component exists with a real agent ID
|
||||
- User only wants to update styling or dimensions
|
||||
|
||||
### Step 3: Canonical import strategy
|
||||
|
||||
Use this import path by default in app code:
|
||||
|
||||
```tsx
|
||||
import { AgentforceConversationClient } from "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental";
|
||||
```
|
||||
|
||||
If the package is not installed, install it:
|
||||
|
||||
```bash
|
||||
npm install @salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental
|
||||
```
|
||||
|
||||
Only use a local relative import (for example, `./components/AgentforceConversationClient`) when the user explicitly asks to use a patched/local component in that app.
|
||||
|
||||
Do not infer import path from file discovery alone. Prefer one consistent package import across the codebase.
|
||||
|
||||
### Step 4: Add or update component
|
||||
|
||||
**For new installations:**
|
||||
|
||||
Add to the target React component file using the canonical package import:
|
||||
|
||||
```tsx
|
||||
import { Outlet } from "react-router";
|
||||
import { AgentforceConversationClient } from "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental";
|
||||
|
||||
export default function AgentChatHost() {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<AgentforceConversationClient agentId="0Xx..." />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Fallback note:** Use a local relative import only when the user explicitly requests patched/local component usage in that app.
|
||||
|
||||
**For updates:**
|
||||
|
||||
Read the file where component is used and modify only the props that need to change. Preserve all other props. Never delete and recreate.
|
||||
|
||||
**Replacing placeholder values:**
|
||||
|
||||
If the component has a placeholder agentId (e.g., `agentId="Placeholder"` or `agentId="0Xx..."`), replace it with the real agent ID:
|
||||
|
||||
```tsx
|
||||
// Before (template with placeholder)
|
||||
<AgentforceConversationClient agentId="Placeholder" />
|
||||
|
||||
// After (with real agent ID)
|
||||
<AgentforceConversationClient agentId="0Xx8X00000001AbCDE" />
|
||||
```
|
||||
|
||||
### Step 5: Configure props
|
||||
|
||||
**Available props (use directly on component):**
|
||||
|
||||
- `agentId` (string, required) - Salesforce agent ID
|
||||
- `inline` (boolean) - `true` for inline mode, omit for floating
|
||||
- `width` (number | string) - e.g., `420` or `"100%"`
|
||||
- `height` (number | string) - e.g., `600` or `"80vh"`
|
||||
- `headerEnabled` (boolean) - Show/hide header
|
||||
- `styleTokens` (object) - For all styling (colors, fonts, spacing)
|
||||
- `salesforceOrigin` (string) - Auto-resolved
|
||||
- `frontdoorUrl` (string) - Auto-resolved
|
||||
|
||||
**Examples:**
|
||||
|
||||
Floating mode (default):
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." />
|
||||
```
|
||||
|
||||
Inline mode with dimensions:
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." inline width="420px" height="600px" />
|
||||
```
|
||||
|
||||
Styling with styleTokens:
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#0176d3",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
messageBlockInboundBackgroundColor: "#4CAF50",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**For complex patterns,** consult `references/examples.md` for:
|
||||
|
||||
- Sidebar containers and responsive sizing
|
||||
- Dark theme and advanced theming combinations
|
||||
- Inline without header, calculated dimensions
|
||||
- Complete host component examples
|
||||
|
||||
**For styling:** For ANY color, font, or spacing changes, use `styleTokens` prop only. See `references/style-tokens.md` for complete token list and examples.
|
||||
|
||||
**Common mistakes to avoid:** Consult `references/constraints.md` for:
|
||||
|
||||
- Invalid props (containerStyle, style, className)
|
||||
- Invalid styling approaches (CSS files, style tags)
|
||||
- What files NOT to edit (implementation files)
|
||||
|
||||
## Common Issues
|
||||
|
||||
If component doesn't appear or authentication fails, see `references/troubleshooting.md` for:
|
||||
|
||||
- Agent activation and deployment
|
||||
- Localhost trusted domains
|
||||
- Cookie restriction settings
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before the component will work, the following Salesforce settings must be configured by the user:
|
||||
|
||||
**Cookie settings:**
|
||||
|
||||
- Setup → My Domain → Disable "Require first party use of Salesforce cookies"
|
||||
|
||||
**Trusted domains (required only for local development):**
|
||||
|
||||
- Setup → Session Settings → Trusted Domains for Inline Frames → Add your domain
|
||||
- Local development: `localhost:<PORT>` (e.g., `localhost:3000`)
|
||||
@ -0,0 +1,134 @@
|
||||
# Constraints and Anti-Patterns
|
||||
|
||||
This document lists all invalid approaches and patterns to avoid when working with AgentforceConversationClient.
|
||||
|
||||
## Never Edit Implementation Files
|
||||
|
||||
**CRITICAL: Only edit files where the component is USED, never the component implementation itself.**
|
||||
|
||||
- ✅ **DO edit**: Any React files that import and use `<AgentforceConversationClient />` (for example, shared shells, route components, or feature pages)
|
||||
- ❌ **DO NOT edit**: AgentforceConversationClient.tsx, AgentforceConversationClient.jsx, index.tsx, index.jsx, or any files inside:
|
||||
- `node_modules/@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental/src/`
|
||||
- `packages/template/feature/feature-react-agentforce-conversation-client/src/`
|
||||
- `src/components/AgentforceConversationClient.tsx` (patched templates)
|
||||
- Any path containing `/components/AgentforceConversationClient.`
|
||||
|
||||
**If you're reading a file named `AgentforceConversationClient.tsx`, you're in the wrong place. Stop and search for the USAGE instead.**
|
||||
|
||||
## Invalid Props
|
||||
|
||||
AgentforceConversationClient uses a flat prop API and does NOT accept these props:
|
||||
|
||||
- ❌ `containerStyle` - Use `width` and `height` props directly instead
|
||||
- ❌ `style` - Use `styleTokens` for theming
|
||||
- ❌ `className` - Not supported
|
||||
- ❌ Any standard React div props - This wraps an embedded iframe, not a div
|
||||
|
||||
**Why:** The component is a wrapper around an embedded iframe using Lightning Out 2.0. Standard React styling props don't apply.
|
||||
|
||||
## Invalid Styling Approaches
|
||||
|
||||
**CRITICAL: For ALL styling, theming, branding, or color changes - ONLY use `styleTokens` prop.**
|
||||
|
||||
Never use these approaches:
|
||||
|
||||
- ❌ Creating CSS files (e.g., `agent-styles.css`, `theme.css`)
|
||||
- ❌ Creating `<style>` tags or internal stylesheets
|
||||
- ❌ Using `style` attribute on the component
|
||||
- ❌ Using `className` prop
|
||||
- ❌ Inline styles
|
||||
- ❌ CSS modules
|
||||
- ❌ Styled-components or any CSS-in-JS libraries
|
||||
|
||||
**Why:** The component controls its own internal styling through the `styleTokens` API. External CSS cannot reach into the embedded iframe.
|
||||
|
||||
## Invalid Implementation Approaches
|
||||
|
||||
Never do these:
|
||||
|
||||
- ❌ Create custom chat UIs from scratch
|
||||
- ❌ Use third-party chat libraries (socket.io, WebSocket libraries, etc.)
|
||||
- ❌ Call `embedAgentforceClient` directly from `@salesforce/agentforce-conversation-client`
|
||||
- ❌ Build custom WebSocket or REST API chat implementations
|
||||
|
||||
**Why:** The AgentforceConversationClient component is the official wrapper that handles authentication, Lightning Out 2.0 initialization, and all communication with Salesforce agents. Custom implementations will not work.
|
||||
|
||||
## Invalid Update Patterns
|
||||
|
||||
When updating an existing component:
|
||||
|
||||
- ❌ Delete and recreate the component
|
||||
- ❌ Remove all props and start over
|
||||
- ❌ Copy the entire component to a new file
|
||||
|
||||
**Why:** This loses configuration, introduces errors, and creates unnecessary diffs. Always update props in place.
|
||||
|
||||
## Examples
|
||||
|
||||
### ❌ Wrong - Using containerStyle
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." containerStyle={{ width: 420, height: 600 }} />
|
||||
```
|
||||
|
||||
### ✅ Correct - Using width/height directly
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." width="420px" height="600px" />
|
||||
```
|
||||
|
||||
### ❌ Wrong - Creating CSS file
|
||||
|
||||
```css
|
||||
/* agent-styles.css */
|
||||
.agentforce-chat {
|
||||
background: red;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import "./agent-styles.css";
|
||||
|
||||
<AgentforceConversationClient className="agentforce-chat" />;
|
||||
```
|
||||
|
||||
### ✅ Correct - Using styleTokens
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "red",
|
||||
headerBlockTextColor: "white",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### ❌ Wrong - Creating style tag
|
||||
|
||||
```tsx
|
||||
<>
|
||||
<style>{`.agent-chat { background: blue; }`}</style>
|
||||
<AgentforceConversationClient agentId="0Xx..." />
|
||||
</>
|
||||
```
|
||||
|
||||
### ✅ Correct - Using styleTokens
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "blue",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### ❌ Wrong - Editing implementation file
|
||||
|
||||
Reading or editing: `node_modules/@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental/src/AgentforceConversationClient.tsx`
|
||||
|
||||
### ✅ Correct - Editing usage file
|
||||
|
||||
Reading and editing: usage files where the component is imported and used (for example, `src/app.tsx`, a route component, or a feature page)
|
||||
@ -0,0 +1,132 @@
|
||||
# Additional Examples
|
||||
|
||||
Essential examples for common patterns and combinations. All use flat props API.
|
||||
|
||||
---
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Sidebar Chat
|
||||
|
||||
```tsx
|
||||
export default function DashboardWithChat() {
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh" }}>
|
||||
<main style={{ flex: 1 }}>{/* Main content */}</main>
|
||||
<aside style={{ width: 400 }}>
|
||||
<AgentforceConversationClient agentId="0Xx..." inline width="100%" height="100%" />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Full Page Chat
|
||||
|
||||
```tsx
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Customer Support</h1>
|
||||
<AgentforceConversationClient agentId="0Xx..." inline width="100%" height="600px" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Size Variations
|
||||
|
||||
### Responsive sizing
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." inline width="100%" height="80vh" />
|
||||
```
|
||||
|
||||
### Calculated dimensions
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx..." inline width="500px" height="calc(100vh - 100px)" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theming Combinations
|
||||
|
||||
### Brand theme with custom sizing
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
inline
|
||||
width="500px"
|
||||
height="700px"
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#0176d3",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
messageBlockInboundBackgroundColor: "#0176d3",
|
||||
messageBlockInboundTextColor: "#ffffff",
|
||||
messageInputFooterSendButton: "#0176d3",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Dark theme
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#1a1a1a",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
messageBlockInboundBackgroundColor: "#2d2d2d",
|
||||
messageBlockInboundTextColor: "#ffffff",
|
||||
messageBlockOutboundBackgroundColor: "#3a3a3a",
|
||||
messageBlockOutboundTextColor: "#f0f0f0",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Inline without header
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
inline
|
||||
width="100%"
|
||||
height="600px"
|
||||
headerEnabled={false}
|
||||
styleTokens={{
|
||||
messageBlockBorderRadius: "12px",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Host Component Example
|
||||
|
||||
```tsx
|
||||
import { Outlet } from "react-router";
|
||||
import { AgentforceConversationClient } from "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental";
|
||||
|
||||
export default function AgentChatHost() {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#0176d3",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For complete style token reference, see `references/style-tokens.md` or `node_modules/@salesforce/agentforce-conversation-client/README.md`.
|
||||
@ -0,0 +1,101 @@
|
||||
# Style Tokens Reference
|
||||
|
||||
This document explains how to use `styleTokens` for theming and styling the AgentforceConversationClient.
|
||||
|
||||
## Overview
|
||||
|
||||
The `styleTokens` prop is the **ONLY** way to customize the appearance of the Agentforce conversation client. It accepts an object with style token keys and CSS values.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
For the complete and always up-to-date list of all 60+ style tokens, see:
|
||||
|
||||
**[@salesforce/agentforce-conversation-client on npm](https://www.npmjs.com/package/@salesforce/agentforce-conversation-client)**
|
||||
|
||||
The npm package README contains the definitive documentation with all available style tokens.
|
||||
|
||||
## Token Categories
|
||||
|
||||
Style tokens are organized by UI area:
|
||||
|
||||
- **Header** (7 tokens): background, text color, hover, active, focus, border, font family
|
||||
- **Messages** (10 tokens): colors, padding, margins, border radius, fonts, body width
|
||||
- **Inbound messages** (5 tokens): background, text color, width, alignment, hover
|
||||
- **Outbound messages** (5 tokens): background, text color, width, alignment, margin
|
||||
- **Input** (33 tokens): colors, borders, fonts, padding, buttons, scrollbar, textarea, actions
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Change header color
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#0176d3",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Change message colors
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
messageBlockInboundBackgroundColor: "#4CAF50",
|
||||
messageBlockInboundTextColor: "#ffffff",
|
||||
messageBlockOutboundBackgroundColor: "#f5f5f5",
|
||||
messageBlockOutboundTextColor: "#333333",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Apply brand colors
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
headerBlockBackground: "#1a73e8",
|
||||
headerBlockTextColor: "#ffffff",
|
||||
messageBlockInboundBackgroundColor: "#1a73e8",
|
||||
messageBlockInboundTextColor: "#ffffff",
|
||||
messageInputFooterSendButton: "#1a73e8",
|
||||
messageInputFooterSendButtonHoverColor: "#1557b0",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Adjust spacing and fonts
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient
|
||||
agentId="0Xx..."
|
||||
styleTokens={{
|
||||
messageInputFontSize: "16px",
|
||||
messageBlockBorderRadius: "12px",
|
||||
messageBlockPadding: "16px",
|
||||
messageInputPadding: "12px",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## How to Find Token Names
|
||||
|
||||
1. Check the [@salesforce/agentforce-conversation-client npm package](https://www.npmjs.com/package/@salesforce/agentforce-conversation-client) for the complete list of all tokens
|
||||
|
||||
2. Token names follow a pattern:
|
||||
- `headerBlock*` - Header area
|
||||
- `messageBlock*` - Message bubbles
|
||||
- `messageBlockInbound*` - Messages from customer to agent
|
||||
- `messageBlockOutbound*` - Messages from agent to customer
|
||||
- `messageInput*` - Input field and send button
|
||||
|
||||
## Important Notes
|
||||
|
||||
- You do NOT need to provide all tokens - only override the ones you want to change
|
||||
- Token values are CSS strings (e.g., `"#FF0000"`, `"16px"`, `"bold"`)
|
||||
- Invalid token names are silently ignored
|
||||
- The component uses default values for any tokens you don't specify
|
||||
@ -0,0 +1,57 @@
|
||||
# Troubleshooting
|
||||
|
||||
Common issues when using the Agentforce Conversation Client.
|
||||
|
||||
---
|
||||
|
||||
### Component throws "requires agentId"
|
||||
|
||||
**Cause:** `agentId` was not passed.
|
||||
|
||||
**Solution:** Pass `agentId` directly as a flat prop:
|
||||
|
||||
```tsx
|
||||
<AgentforceConversationClient agentId="0Xx000000000000AAA" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Chat widget does not appear
|
||||
|
||||
**Cause:** Invalid `agentId` or inactive agent.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Confirm the id is correct (18-char Salesforce id, starts with `0Xx`).
|
||||
2. Ensure the agent is Active in **Setup → Agentforce Agents**.
|
||||
3. Verify the agent is deployed to the target channel.
|
||||
|
||||
---
|
||||
|
||||
### Authentication error on localhost
|
||||
|
||||
**Cause:** `localhost:<PORT>` is not trusted for inline frames.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Go to **Setup → Session Settings → Trusted Domains for Inline Frames**.
|
||||
2. Add `localhost:<PORT>` (example: `localhost:3000`).
|
||||
|
||||
**Important:**
|
||||
|
||||
- This setting should be **temporary for local development only**.
|
||||
- **Remove `localhost:<PORT>` from trusted domains after development**.
|
||||
- **Recommended:** Test the Agentforce conversation client in a deployed app instead of relying on localhost trusted domains for extended periods.
|
||||
|
||||
---
|
||||
|
||||
### Blank iframe / auth session issues
|
||||
|
||||
**Possible cause:** First-party Salesforce cookie restriction may block embedded auth flow in some environments.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Go to **Setup → Session Settings**.
|
||||
2. Find **Require first party use of Salesforce cookies**.
|
||||
3. Disable it **only if needed and approved by your security/admin team**.
|
||||
4. Save and reload.
|
||||
@ -1,84 +0,0 @@
|
||||
---
|
||||
name: salesforce-web-app-creating-records
|
||||
description: Use this skill when users need to create Salesforce records from React web applications. Trigger when users mention createRecord, creating leads/contacts/custom objects from web apps, handling record IDs, form submissions to Salesforce, or Application__c custom objects. Always use this skill for any record creation from React apps.
|
||||
---
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Create Salesforce records from web applications
|
||||
- Implement createRecord functionality for custom or standard objects
|
||||
- Handle record ID extraction from create responses
|
||||
- Troubleshoot deployment errors related to record creation
|
||||
|
||||
|
||||
# Creating Salesforce records (webApplication)
|
||||
|
||||
## Overview
|
||||
Implement list and create functionality for any custom object using GraphQL queries and createRecord API with automatic list refresh.
|
||||
|
||||
## API
|
||||
|
||||
- Use **createRecord** from `@salesforce/webapp-experimental/api`:
|
||||
- `createRecord(objectApiName: string, fields: Record<string, unknown>)` → returns a result object that may contain the new record id in different shapes depending on the API version.
|
||||
|
||||
## Getting the new record id
|
||||
|
||||
The create response is not always a simple `{ id: string }`. Handle both common shapes so you don't get "Create succeeded but no record id returned":
|
||||
|
||||
- Prefer **result.id** when it's a string.
|
||||
- Else read **result.fields.Id.value** (or equivalent) if the API returns the id inside a fields wrapper.
|
||||
|
||||
Example helper:
|
||||
|
||||
```ts
|
||||
function getRecordIdFromResponse(result: Record<string, unknown>): string {
|
||||
const id =
|
||||
typeof result.id === "string"
|
||||
? result.id
|
||||
: (result.fields as Record<string, { value?: string }> | undefined)?.Id?.value;
|
||||
if (!id) throw new Error("Create succeeded but no record id returned");
|
||||
return id;
|
||||
}
|
||||
```
|
||||
|
||||
Use this after `createRecord()` and return `{ id }` to the caller so the UI can show success or navigate.
|
||||
|
||||
## Field set and org schema
|
||||
|
||||
- **Only send fields that exist in the org.** If you send a field that doesn't exist (e.g. custom field not deployed), the API can return POST body parse errors (e.g. "Field X does not exist").
|
||||
- For **custom objects**, deploy the object and its fields (e.g. via SFDX/CLI or metadata API) before relying on them in the app.
|
||||
- **Fallback:** If you need to capture data that might not have a custom field yet (e.g. contact details), store it in a long text area or similar (e.g. `Employment_Info__c`) as a blob (e.g. JSON or line-based text) so no data is lost and the create still succeeds.
|
||||
|
||||
## Custom objects (e.g. Application__c)
|
||||
|
||||
- Define the object and fields in the project's Salesforce metadata (e.g. `objects/Application__c/`, `fields/*.field-meta.xml`).
|
||||
- In the app, build a `fields` object with only the API names and values you want to set; omit required fields only if they have defaults.
|
||||
- Use a typed input interface and map it to the `fields` passed to `createRecord`; optionally combine contact/extra info into one blob field if some fields might not be deployed.
|
||||
|
||||
## Standard objects (e.g. Lead)
|
||||
|
||||
- Use standard field API names: **FirstName**, **LastName**, **Email**, **Company**, **Phone**, **Description**, **LeadSource**, etc.
|
||||
- **LeadSource** helps distinguish origin (e.g. "Website", "Website Newsletter").
|
||||
- For "Contact Us" → Lead: map subject to **Company** (or a custom field if available), message to **Description**.
|
||||
- For newsletter signup → Lead: set **Email**; use a placeholder **LastName** (e.g. "Newsletter Subscriber") and **Company** (e.g. "Website") so the Lead is valid.
|
||||
|
||||
## Structure
|
||||
|
||||
| Concern | Where |
|
||||
|--------|--------|
|
||||
| Create custom object (e.g. Application) | e.g. `src/api/applicationApi.ts` — `createApplicationRecord(input)` → `createRecord("Application__c", fields)` |
|
||||
| Create standard object (e.g. Lead) | e.g. `src/api/leadApi.ts` — `createContactUsLead(input)`, `createNewsletterLead(email)` |
|
||||
| Form UI | Pages that collect data and call these APIs; show success/error and optionally redirect or reset form |
|
||||
|
||||
## Errors
|
||||
|
||||
- **"Field X does not exist"** → Remove that field from the payload or deploy the field to the org.
|
||||
- **"Create succeeded but no record id returned"** → Use the id-extraction pattern above (result.id or result.fields.Id.value).
|
||||
- **Validation errors** → Return and display the API error message; fix required/invalid values in the form.
|
||||
|
||||
## Verification
|
||||
|
||||
- Deploy object/fields to the org if using custom objects.
|
||||
- Run the create flow in the app; confirm the record appears in the org and the UI shows success and the new id if needed.
|
||||
- Test with minimal required fields first, then add optional or blob fields.
|
||||
@ -1,70 +0,0 @@
|
||||
---
|
||||
name: salesforce-web-app-feature
|
||||
description: Use this skill when users need to add features to Salesforce React web applications. Trigger when users mention adding authentication, search, charts, GraphQL, ShadCN components, Agentforce conversation client, or any feature packages. Also use when users want to install npm packages, copy-then-adjust workflow, or integrate official feature packages. This is the PRIMARY skill for web app features - always prefer official packages listed here.
|
||||
---
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Add features to Salesforce React web applications
|
||||
- Install and integrate feature packages (authentication, search, charts, etc.)
|
||||
- Follow copy-then-adjust workflow for feature integration
|
||||
- Troubleshoot deployment errors related to web application features
|
||||
|
||||
|
||||
# Adding a new webApplication feature
|
||||
|
||||
**Always prefer the features listed below.** When the user asks to add auth, search, charts, navigation, GraphQL, shared UI, or Agentforce conversation (ACC/copilot/agent) to a webapp, match their request to one of the official feature packages in the table in section 1. Use those packages first; only build from scratch or use other solutions when no listed feature fits.
|
||||
|
||||
When the user asks to add a feature to their app, follow this workflow.
|
||||
|
||||
When adding a feature, integrating code from an npm package, or bringing in a reference implementation:
|
||||
|
||||
1. **Prefer copying over rewriting.** Use `cp` (or equivalent) to copy files from the source (e.g. `node_modules/<package>/dist/...` or a reference app) into this project. Do not retype or rewrite the same code by hand.
|
||||
|
||||
2. **Then adjust.** After copying, do minimal edits: fix import paths (e.g. change relative `../../` imports to the project's path alias like `@/`), update any app-specific config, and remove or adapt anything that doesn't apply.
|
||||
|
||||
3. **When to copy.** Copy when:
|
||||
- Installing a feature from a template/feature package (e.g. authentication, search, charts).
|
||||
- The package ships full source in `dist/` or `src/` that is meant to be integrated.
|
||||
- You would otherwise be recreating multiple files by reading a reference and typing them out.
|
||||
|
||||
4. **When rewriting is okay.** Only rewrite or create from scratch when:
|
||||
- The source is not file-based (e.g. only docs or snippets).
|
||||
- The integration is a thin wrapper or a single small file.
|
||||
- Copying would pull in a large, unrelated tree and the actual need is a small part of it.
|
||||
|
||||
## 1. Match the request to a feature
|
||||
|
||||
**Prefer these features.** Always check this table first; use the matching package when it fits the user's need.
|
||||
|
||||
Available features (npm packages):
|
||||
|
||||
| Feature | Package | Description | Integration notes |
|
||||
|------------------------------------| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |-------------------------------------------------------------------------|
|
||||
| **Authentication** | `@salesforce/webapp-template-feature-react-authentication-experimental` | Login, register, password reset, protected routes | Copy-then-adjust; fix imports to `@/`; align with app layout and routes |
|
||||
| **Global search** | `@salesforce/webapp-template-feature-react-global-search-experimental` | Search single Salesforce objects with filters and pagination | Copy-then-adjust; fix imports to `@/`; align with app layout and routes |
|
||||
| **Charts** | `@salesforce/webapp-template-feature-react-chart-experimental` | Recharts line/bar charts with theming (AnalyticsChart, ChartContainer) | Copy-then-adjust; fix imports to `@/`; align with app layout and routes |
|
||||
| **GraphQL data access** | `@salesforce/webapp-template-feature-graphql-experimental` | executeGraphQL utilities, codegen tooling, and example AccountsTable | Copy-then-adjust; fix imports to `@/` and `@api/` |
|
||||
| **Shared UI (shadcn)** | `@salesforce/webapp-template-feature-react-shadcn-experimental` | Button, Card, Input, Select, Table, Tabs, and other ShadCN components | Copy-then-adjust per README/AGENT.md |
|
||||
| **Agentforce conversation client** | `@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental` | React wrapper for embedded Agentforce conversation client (agent chat UI) via Lightning Out 2.0; automatic auth resolution; `<AgentforceConversationClient />` component | Import from package (not copy) per README/AGENT.md |
|
||||
|
||||
If no feature matches, tell the user and offer to build it from scratch following the project's existing patterns. Do not substitute third-party or custom implementations when one of the features above matches—always prefer the listed packages.
|
||||
|
||||
## 2. Install the npm package
|
||||
|
||||
```bash
|
||||
npm install <package-name>
|
||||
```
|
||||
|
||||
## 3. Read the README.md / AGENT.md
|
||||
|
||||
The `node_modules` folder of the installed package contains a README.md and/or AGENT.md. Load it and follow its instructions.
|
||||
|
||||
## 4. Always validate
|
||||
|
||||
After integrating, **always validate** with:
|
||||
|
||||
```bash
|
||||
npm i && npm run build && npm run dev
|
||||
```
|
||||
@ -1,36 +0,0 @@
|
||||
---
|
||||
name: salesforce-web-app-list-and-create-records
|
||||
description: Use this skill when users need to list and create records for Salesforce custom objects from React web apps. Trigger when users mention list and create patterns, GraphQL queries, refetch after create, form picklists matching object schema, or dashboard widgets showing record lists. Always use this skill for list+create patterns.
|
||||
---
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Implement list and create functionality for custom objects
|
||||
- Build GraphQL queries for listing records
|
||||
- Create records and update lists without page reload
|
||||
- Troubleshoot deployment errors related to list and create operations
|
||||
|
||||
|
||||
# List and create records (webApplication)
|
||||
|
||||
## Pattern
|
||||
|
||||
- **List:** Query the object via **GraphQL** (`executeGraphQL`, `uiapi.query.ObjectApiName__c`). Use **webApplicationFeature** (GraphQL) for the connection shape (first/after, edges.node, field `{ value, displayValue }`). Map nodes to a simple summary type; sort client-side by date or other field if needed.
|
||||
- **Create:** Use **createRecord** from `@salesforce/webapp-experimental/api`. See **webApplicationCreatingRecords** for required/optional fields and id handling.
|
||||
- **Hook:** One hook that fetches the list on mount and exposes `refetch`. After a successful create, call `refetch()` so the list updates without a full page reload.
|
||||
- **Form:** Collect only fields that exist on the object. For picklist fields, use option values that **match the object's value set** (e.g. from the object's field metadata or a known value set). Default required picklists (e.g. Status to "New", Priority to "Standard") when the object defines defaults.
|
||||
- **UI:** Table or cards for the list; form above or on a separate route. Show loading and error states for both list and submit. Optional: dashboard widget showing a slice of the list (e.g. first N items) with a "See all" link.
|
||||
|
||||
## Structure (generic)
|
||||
|
||||
| Concern | Where |
|
||||
|--------|--------|
|
||||
| API: list + create | e.g. `src/api/<objectName>Api.ts` — query function (GraphQL) and create function (createRecord) |
|
||||
| Hook: list + refetch | e.g. `src/hooks/use<ObjectName>List.ts` — returns `{ items, loading, error, refetch }` |
|
||||
| Page | Form (controlled inputs, submit → create → refetch) and table/list of items |
|
||||
|
||||
## Cross-references
|
||||
|
||||
- **webApplicationFeature** — Feature packages including GraphQL (connection shape, node shape, field value extraction).
|
||||
- **webApplicationCreatingRecords** — createRecord, id extraction, only send existing fields, picklist/required handling.
|
||||
@ -1,34 +0,0 @@
|
||||
---
|
||||
name: salesforce-web-application
|
||||
description: Directory of web application (React) sub-knowledges for Salesforce React BYO, feature packages, and copy-then-adjust workflow. Use when working with Salesforce web applications.
|
||||
---
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Work with Salesforce React web applications
|
||||
- Understand available web application features and workflows
|
||||
- Navigate web application knowledge resources
|
||||
- Troubleshoot deployment errors related to web applications
|
||||
|
||||
## Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides guidance for working with Salesforce React web applications (Salesforce BYO React template). It covers adding features, creating Salesforce records, and listing/creating custom object records.
|
||||
|
||||
## Web Applications — Directory
|
||||
|
||||
This is the **directory** of sub-knowledges for web applications (Salesforce React BYO, feature packages, copy-then-adjust workflow). **Call `get_expert_knowledge` again with one of the topic names below** to load the relevant knowledge.
|
||||
|
||||
## Sub-knowledges (use as `topic` in get_expert_knowledge)
|
||||
|
||||
| Topic name | Use when |
|
||||
|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **webApplicationFeature** | Adding a feature to a webapp: authentication, search, charts, GraphQL, ShadCN, Agentforce conversation client. Feature table, npm install, copy-then-adjust workflow |
|
||||
| **webApplicationCreatingRecords** | Creating Salesforce records: createRecord, custom/standard objects, id handling, Application__c, Lead |
|
||||
| **webApplicationListAndCreateRecords** | List + create any custom object: GraphQL list, createRecord, hook with refetch, form picklists match object |
|
||||
|
||||
**Flow:** After reading this directory, call `get_expert_knowledge({ topic: "<subTopicName>" })` with the single sub-topic that best matches the user's request (e.g. `webApplicationFeature`, `webApplicationCreatingRecords`).
|
||||
|
||||
**Adding features:** Always prefer the feature packages listed in **webApplicationFeature** (authentication, search, charts, nav, GraphQL, ShadCN, Agentforce conversation) over building from scratch or other solutions when one of them matches the request.
|
||||
324
skills/using-webapp-graphql/SKILL.md
Normal file
324
skills/using-webapp-graphql/SKILL.md
Normal file
@ -0,0 +1,324 @@
|
||||
---
|
||||
name: using-webapp-graphql
|
||||
description: Salesforce GraphQL data access. Use when the user asks to fetch, query, or mutate Salesforce data, or add a GraphQL operation for an object like Account, Contact, or Opportunity.
|
||||
paths:
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.graphql"
|
||||
---
|
||||
|
||||
# Salesforce GraphQL
|
||||
|
||||
Guidance for querying and mutating Salesforce data via the Salesforce GraphQL API. Use `createDataSDK()` + `sdk.graphql?.()` and codegen tooling.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks to "fetch data from Salesforce"
|
||||
- User asks to "query" or "mutate" Salesforce records
|
||||
- User wants to add a new GraphQL operation (query or mutation)
|
||||
- User asks to add data access for a Salesforce object (Account, Contact, Opportunity, etc.)
|
||||
|
||||
## Schema Access Policy (GREP ONLY)
|
||||
|
||||
> **GREP ONLY** — The `schema.graphql` file is very large (~265,000+ lines). All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep.
|
||||
|
||||
## Directory Context
|
||||
|
||||
The generated app has a two-level directory structure. Commands must run from the correct directory.
|
||||
|
||||
```
|
||||
<project-root>/ ← SFDX project root
|
||||
├── schema.graphql ← grep target
|
||||
├── sfdx-project.json
|
||||
└── force-app/main/default/webapplications/<app-name>/ ← webapp dir
|
||||
├── package.json (npm scripts: graphql:schema, graphql:codegen, lint)
|
||||
├── eslint.config.js (schema ref: ../../../../../schema.graphql)
|
||||
├── codegen.yml (schema ref: ../../../../../schema.graphql)
|
||||
└── src/ (source code, .graphql query files)
|
||||
```
|
||||
|
||||
| Command | Run from | Why |
|
||||
| ------------------------- | ---------------- | -------------------------------------- |
|
||||
| `npm run graphql:schema` | **webapp dir** | Script is in webapp's `package.json` |
|
||||
| `npm run graphql:codegen` | **webapp dir** | Reads `codegen.yml` in webapp dir |
|
||||
| `npx eslint <file>` | **webapp dir** | Reads `eslint.config.js` in webapp dir |
|
||||
| `grep ... schema.graphql` | **project root** | `schema.graphql` lives at project root |
|
||||
| `sf api request graphql` | **project root** | Needs `sfdx-project.json` |
|
||||
|
||||
> **Wrong directory = silent failures.** `npm run graphql:schema` from the project root will fail with "missing script." `grep ./schema.graphql` from the webapp dir will fail with "no such file."
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The base React app (`base-react-app`) ships with all GraphQL dependencies and tooling pre-configured:
|
||||
|
||||
- `@salesforce/sdk-data` — runtime SDK for `createDataSDK` and `gql`
|
||||
- `@graphql-codegen/cli` + plugins — type generation from `.graphql` files and inline `gql` queries
|
||||
- `@graphql-eslint/eslint-plugin` — validates `.graphql` files and `gql` template literals against `schema.graphql` (used as a query validation gate — see Step 6)
|
||||
- `graphql` — shared by codegen, ESLint, and schema introspection
|
||||
|
||||
Before using this skill, ensure:
|
||||
|
||||
1. The `@salesforce/sdk-data` package is available (provides `createDataSDK`, `gql`, `NodeOfConnection`)
|
||||
2. **Deployment order**: Metadata must be deployed before schema fetch; schema must be refetched after any metadata deployment. Invoke the `deploying-to-salesforce` skill when deploying or syncing with the org.
|
||||
3. A `schema.graphql` file exists at the project root. If missing, generate it:
|
||||
```bash
|
||||
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
|
||||
npm run graphql:schema
|
||||
```
|
||||
|
||||
## npm Scripts
|
||||
|
||||
- **`npm run graphql:schema`** — _(run from webapp dir)_ Downloads the full GraphQL schema from a connected Salesforce org via introspection. Outputs `schema.graphql` to the project root.
|
||||
- **`npm run graphql:codegen`** — _(run from webapp dir)_ Generates TypeScript types from `.graphql` files and inline `gql` queries. Outputs to `src/api/graphql-operations-types.ts`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Download Schema
|
||||
|
||||
Ensure `schema.graphql` exists at the project root. If missing, run `npm run graphql:schema` from the webapp dir.
|
||||
|
||||
### Step 2: Explore the Schema (grep-only)
|
||||
|
||||
Before writing any query, verify the target object and its fields exist in the schema.
|
||||
|
||||
**Invoke the `exploring-graphql-schema` skill** for the full exploration workflow and **mandatory grep-only access policy**.
|
||||
|
||||
> **GREP ONLY** — All schema lookups MUST use the grep commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep.
|
||||
|
||||
Key actions (all via grep):
|
||||
|
||||
- `type <ObjectName> implements Record` — find available fields
|
||||
- `input <ObjectName>_Filter` — find filter options
|
||||
- `input <ObjectName>_OrderBy` — find sorting options
|
||||
- `input <ObjectName>CreateInput` / `<ObjectName>UpdateInput` — find mutation input types
|
||||
|
||||
### Step 3: Choose the Query Pattern
|
||||
|
||||
**Pattern 1 — External `.graphql` file** (recommended for complex queries):
|
||||
|
||||
- Queries with variables, fragments, or shared across files
|
||||
- Full codegen support, syntax highlighting, shareable
|
||||
- Requires codegen step after changes
|
||||
- See example: `api/utils/accounts.ts` + `api/utils/query/highRevenueAccountsQuery.graphql`
|
||||
|
||||
**Pattern 2 — Inline `gql` tag** (recommended for simple queries):
|
||||
|
||||
- Simple queries without variables; colocated with usage code
|
||||
- Supports dynamic queries (field set varies at runtime)
|
||||
- **MUST use `gql` tag** — plain template strings bypass `@graphql-eslint` validation
|
||||
- See example: `api/utils/user.ts`
|
||||
|
||||
### Step 4: Write the Query
|
||||
|
||||
For **Pattern 1**:
|
||||
|
||||
1. Create a `.graphql` file under `src/api/utils/query/`
|
||||
2. Follow UIAPI structure: `query { uiapi { query { ObjectName(...) { edges { node { ... } } } } } }`
|
||||
3. For mutations, invoke the `generating-graphql-mutation-query` skill
|
||||
4. For read queries, invoke the `generating-graphql-read-query` skill
|
||||
|
||||
For **Pattern 2**:
|
||||
|
||||
1. Define query inline using the `gql` template tag
|
||||
2. Ensure the query name matches what codegen expects
|
||||
|
||||
### Step 5: Test Queries Against Live Org
|
||||
|
||||
Use the testing workflows in the `generating-graphql-read-query` and `generating-graphql-mutation-query` skills to validate queries against the connected org before integrating into the app.
|
||||
|
||||
### Step 6: Generate Types
|
||||
|
||||
```bash
|
||||
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
|
||||
npm run graphql:codegen
|
||||
```
|
||||
|
||||
This updates `src/api/graphql-operations-types.ts` with `<OperationName>Query`/`<OperationName>Mutation` and `<OperationName>QueryVariables`/`<OperationName>MutationVariables`.
|
||||
|
||||
### Step 7: Lint Validate
|
||||
|
||||
Run ESLint on the file containing the query to validate it against the schema **before** any live testing:
|
||||
|
||||
```bash
|
||||
# Run from webapp dir
|
||||
npx eslint <path-to-file>
|
||||
```
|
||||
|
||||
The `@graphql-eslint/eslint-plugin` processor extracts GraphQL from `gql` template literals and validates them against `schema.graphql`. Fix all ESLint errors before proceeding.
|
||||
|
||||
### Step 8: Implement and Verify
|
||||
|
||||
Implement the data access function using the pattern below. Use the Quality Checklist before completing.
|
||||
|
||||
---
|
||||
|
||||
## Core Types & Function Signatures
|
||||
|
||||
### createDataSDK and graphql
|
||||
|
||||
```typescript
|
||||
import { createDataSDK } from "@salesforce/sdk-data";
|
||||
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.graphql?.<ResponseType, VariablesType>(query, variables);
|
||||
```
|
||||
|
||||
`createDataSDK()` returns a `DataSDK` instance. The `graphql` method uses optional chaining (`?.`) because not all surfaces support GraphQL.
|
||||
|
||||
### gql Template Tag
|
||||
|
||||
```typescript
|
||||
import { gql } from "@salesforce/sdk-data";
|
||||
|
||||
const MY_QUERY = gql`
|
||||
query MyQuery {
|
||||
uiapi { ... }
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
The `gql` tag enables ESLint validation against the schema. Plain template strings bypass validation.
|
||||
|
||||
### Error Handling
|
||||
|
||||
Default: treat any errors as failure (Strategy A). For partial data tolerance, log errors but use data. For mutations where some return fields are inaccessible, use Strategy C (fail only when no data).
|
||||
|
||||
```typescript
|
||||
// Default: strict
|
||||
if (response?.errors?.length) {
|
||||
throw new Error(response.errors.map((e) => e.message).join("; "));
|
||||
}
|
||||
const result = response?.data;
|
||||
```
|
||||
|
||||
Responses follow `uiapi.query.ObjectName.edges[].node`; fields use `{ value }`.
|
||||
|
||||
### NodeOfConnection
|
||||
|
||||
```typescript
|
||||
import { type NodeOfConnection } from "@salesforce/sdk-data";
|
||||
|
||||
type AccountNode = NodeOfConnection<GetHighRevenueAccountsQuery["uiapi"]["query"]["Account"]>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: External .graphql File
|
||||
|
||||
Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix, and use generated types.
|
||||
|
||||
**Required imports:**
|
||||
|
||||
```typescript
|
||||
import { createDataSDK, type NodeOfConnection } from "@salesforce/sdk-data";
|
||||
import MY_QUERY from "./query/myQuery.graphql?raw"; // ← ?raw suffix required
|
||||
import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
|
||||
```
|
||||
|
||||
**When to use:** Complex queries with variables, fragments, or shared across files. Does NOT support dynamic queries (field set varies at runtime).
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Inline gql Tag
|
||||
|
||||
**Required imports:**
|
||||
|
||||
```typescript
|
||||
import { createDataSDK, gql } from "@salesforce/sdk-data";
|
||||
import { type CurrentUserQuery } from "../graphql-operations-types";
|
||||
|
||||
const MY_QUERY = gql`
|
||||
query CurrentUser {
|
||||
uiapi { ... }
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
> **MUST use `gql` tag** — plain template strings bypass the `@graphql-eslint` processor entirely, meaning no lint validation against the schema.
|
||||
|
||||
**When to use:** Simple, colocated queries. Supports dynamic queries (field set varies at runtime).
|
||||
|
||||
---
|
||||
|
||||
## Conditional Field Selection
|
||||
|
||||
For dynamic fieldsets with **known** fields, use `@include(if: $condition)` and `@skip(if: $condition)` in `.graphql` files. See GraphQL spec for details.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Not Recommended)
|
||||
|
||||
### Direct API Calls
|
||||
|
||||
```typescript
|
||||
// NOT RECOMMENDED: Direct axios/fetch calls for GraphQL
|
||||
// PREFERRED: Use the Data SDK
|
||||
const sdk = await createDataSDK();
|
||||
const response = await sdk.graphql?.<ResponseType>(query, variables);
|
||||
```
|
||||
|
||||
### Missing Type Definitions
|
||||
|
||||
```typescript
|
||||
// NOT RECOMMENDED: Untyped GraphQL calls
|
||||
// PREFERRED: Provide response type
|
||||
const response = await sdk.graphql?.<GetMyDataQuery>(query);
|
||||
```
|
||||
|
||||
### Plain String Queries (Without gql Tag)
|
||||
|
||||
```typescript
|
||||
// NOT RECOMMENDED: Plain strings bypass ESLint validation
|
||||
const query = `query { ... }`;
|
||||
|
||||
// PREFERRED: Use gql tag for inline queries
|
||||
const QUERY = gql`query { ... }`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
> If you have not completed the workflow above, **stop and complete it first**. Invoke the skill workflow before using this checklist.
|
||||
|
||||
Before completing GraphQL data access code:
|
||||
|
||||
### For Pattern 1 (.graphql files):
|
||||
|
||||
1. [ ] All field names verified via grep against `schema.graphql` (invoke `exploring-graphql-schema`)
|
||||
2. [ ] Create `.graphql` file for the query/mutation
|
||||
3. [ ] Run `npm run graphql:codegen` to generate types
|
||||
4. [ ] Import query with `?raw` suffix
|
||||
5. [ ] Import generated types from `graphql-operations-types.ts`
|
||||
6. [ ] Use `sdk.graphql?.<ResponseType>()` with proper generic
|
||||
7. [ ] Handle `response.errors` and destructure `response.data`
|
||||
8. [ ] Use `NodeOfConnection` for cleaner node types when needed
|
||||
9. [ ] Run `npx eslint <file>` from webapp dir — fix all GraphQL errors
|
||||
|
||||
### For Pattern 2 (inline with gql):
|
||||
|
||||
1. [ ] All field names verified via grep against `schema.graphql`
|
||||
2. [ ] Define query using `gql` template tag (NOT a plain string)
|
||||
3. [ ] Ensure query name matches generated types in `graphql-operations-types.ts`
|
||||
4. [ ] Import generated types for the query
|
||||
5. [ ] Use `sdk.graphql?.<ResponseType>()` with proper generic
|
||||
6. [ ] Handle `response.errors` and destructure `response.data`
|
||||
7. [ ] Run `npx eslint <file>` from webapp dir — fix all GraphQL errors
|
||||
|
||||
### General:
|
||||
|
||||
- [ ] Lint validation passes (`npx eslint <file>` reports no GraphQL errors)
|
||||
- [ ] Query field names match the schema exactly (case-sensitive, confirmed via grep)
|
||||
- [ ] Response type generic is provided to `sdk.graphql?.<T>()`
|
||||
- [ ] Optional chaining is used for nested response data
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- Schema exploration: invoke the `exploring-graphql-schema` skill
|
||||
- Read query generation: invoke the `generating-graphql-read-query` skill
|
||||
- Mutation query generation: invoke the `generating-graphql-mutation-query` skill
|
||||
- Shared GraphQL schema types: `shared-schema.graphqls` (in this skill directory)
|
||||
- Schema download: `npm run graphql:schema` (run from webapp dir)
|
||||
- Type generation: `npm run graphql:codegen` (run from webapp dir)
|
||||
1150
skills/using-webapp-graphql/shared-schema.graphqls
Normal file
1150
skills/using-webapp-graphql/shared-schema.graphqls
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user