chore: sync React b2e and b2x samples from npm @1.117.3
@ -1 +1 @@
|
||||
1.116.6
|
||||
1.117.3
|
||||
|
||||
@ -1,87 +1,193 @@
|
||||
# Agent guide: SFDX project with React web app
|
||||
# Agent guide: Salesforce UI Bundle development
|
||||
|
||||
This project is a **Salesforce DX (SFDX) project** containing a **React web application**. The SFDX source path is defined in `sfdx-project.json` (`packageDirectories[].path`); the web app lives under `<sfdx-source>/webapplications/<appName>/`. Use this file when working in this directory.
|
||||
This project is a **Salesforce DX (SFDX) project** containing a **React UI Bundle**. The UI Bundle is a standalone Vite + React SPA that runs inside the Salesforce platform. Use this file when working in this project.
|
||||
|
||||
## SFDX Source Path
|
||||
## Resolving paths
|
||||
|
||||
The source path prefix is **not** always `force-app`. Read `sfdx-project.json` at the project root, take the first `packageDirectories[].path` value, and append `/main/default` to get `<sfdx-source>`. All paths below use this placeholder.
|
||||
Read `sfdx-project.json` at the project root. Take the first `packageDirectories[].path` value and append `/main/default` to get `<sfdx-source>`. The UI Bundle directory is:
|
||||
|
||||
```
|
||||
<sfdx-source>/uiBundles/<appName>/
|
||||
```
|
||||
|
||||
Replace `<appName>` with the actual folder name found under `uiBundles/`. The source path is **not** always `force-app` — always resolve it from `sfdx-project.json`.
|
||||
|
||||
## Project layout
|
||||
|
||||
- **Project root**: this directory — SFDX project root. Contains `sfdx-project.json`, the SFDX source directory, and (optionally) LWC/Aura.
|
||||
- **React web app**: `<sfdx-source>/webapplications/<appName>/`
|
||||
- Replace `<appName>` with the actual app folder name (e.g. `base-react-app`, or the name chosen when the app was generated).
|
||||
- Entry: `src/App.tsx`
|
||||
- Routes: `src/routes.tsx`
|
||||
- API/GraphQL: `src/api/` (e.g. `graphql.ts`, `graphql-operations-types.ts`, `utils/`)
|
||||
```
|
||||
<project-root>/
|
||||
├── sfdx-project.json
|
||||
├── package.json # SFDX root scripts
|
||||
├── scripts/
|
||||
│ ├── setup-cli.mjs # One-command setup (deploy, schema, build)
|
||||
│ └── graphql-search.sh # Schema entity lookup
|
||||
├── config/
|
||||
│ └── project-scratch-def.json
|
||||
│
|
||||
└── <sfdx-source>/
|
||||
├── uiBundles/
|
||||
│ └── <appName>/ # ← React UI Bundle (primary workspace)
|
||||
│ ├── <appName>.uibundle-meta.xml
|
||||
│ ├── ui-bundle.json
|
||||
│ ├── index.html
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts / tsconfig.json
|
||||
│ ├── vitest.config.ts / playwright.config.ts
|
||||
│ ├── codegen.yml / .graphqlrc.yml
|
||||
│ └── src/ # All application code lives here
|
||||
│
|
||||
├── classes/ # Apex classes (optional)
|
||||
├── objects/ # Custom objects and fields (optional)
|
||||
├── permissionsets/ # Permission sets (optional)
|
||||
├── cspTrustedSites/ # CSP trusted site definitions (optional)
|
||||
├── layouts/ # Object layouts (optional)
|
||||
├── triggers/ # Apex triggers (optional)
|
||||
└── data/ # Sample data for import (optional)
|
||||
```
|
||||
|
||||
Path convention: **webapplications** (lowercase).
|
||||
## Web application source structure
|
||||
|
||||
All application code lives inside the UI Bundle's `src/` directory:
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.tsx # Entry point — creates the browser router
|
||||
├── appLayout.tsx # Shell layout (header, navigation, Outlet, footer)
|
||||
├── routes.tsx # Single route registry for the entire app
|
||||
├── navigationMenu.tsx # Navigation component
|
||||
├── router-utils.tsx # Router helpers
|
||||
├── lib/utils.ts # Utility functions (cn, etc.)
|
||||
├── styles/global.css # Tailwind global styles
|
||||
├── api/ # GraphQL operations, clients, data services
|
||||
├── assets/ # Static SVGs, images
|
||||
├── components/
|
||||
│ ├── ui/ # Shared primitives (shadcn-style: button, card, input, etc.)
|
||||
│ ├── layout/ # Layout components (header, footer, sidebar)
|
||||
│ └── <feature>/ # Feature-specific components
|
||||
├── features/ # Feature modules (auth, search, etc.)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── pages/ # Page components (one per route)
|
||||
├── public/ # Static assets served as-is
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
### Key files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `app.tsx` | Creates `BrowserRouter`; do not add UI here |
|
||||
| `appLayout.tsx` | Source of truth for navigation, header, footer, and page shell |
|
||||
| `routes.tsx` | Single route registry; all pages are children of the layout route |
|
||||
| `<appName>.uibundle-meta.xml` | Salesforce deploy descriptor (`masterLabel`, `version`, `isActive`) |
|
||||
| `ui-bundle.json` | Runtime config (`outputDir`, routing) |
|
||||
|
||||
## Two package.json contexts
|
||||
|
||||
### 1. Project root (this directory)
|
||||
### 1. Project root
|
||||
|
||||
Used for SFDX metadata (LWC, Aura, etc.). Scripts here are for the base SFDX template:
|
||||
Used for SFDX metadata tooling. Scripts here target LWC/Aura, not the React app.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `npm run lint` | ESLint for `aura/` and `lwc/` |
|
||||
| `npm run test` | LWC Jest (passWithNoTests) |
|
||||
| `npm run prettier` | Format supported metadata files |
|
||||
| `npm run prettier` | Format metadata files |
|
||||
| `npm run prettier:verify` | Check Prettier |
|
||||
|
||||
**One-command setup:** From project root run `node scripts/setup-cli.mjs --target-org <alias>` to run login (if needed), deploy, optional permset/data import, GraphQL schema/codegen, web app build, and optionally the dev server. Use `node scripts/setup-cli.mjs --help` for options (e.g. `--skip-login`, `--skip-data`, `--webapp-name`).
|
||||
**One-command setup:** `node scripts/setup-cli.mjs --target-org <alias>` runs login, deploy, permset assignment, data import, GraphQL schema/codegen, UI Bundle build, and optionally the dev server. Use `--help` for all flags.
|
||||
|
||||
Root **does not** run the React app. The root `npm run build` is a no-op for the base SFDX project.
|
||||
### 2. Web app directory (primary workspace)
|
||||
|
||||
### 2. React web app (where you do most work)
|
||||
|
||||
**Always `cd` into the web app directory for dev/build/lint/test:**
|
||||
|
||||
```bash
|
||||
cd <sfdx-source>/webapplications/<appName>
|
||||
```
|
||||
**Always `cd` into the UI Bundle directory for dev/build/lint/test:**
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `npm run dev` | Start Vite dev server |
|
||||
| `npm run build` | TypeScript (`tsc -b`) + Vite build |
|
||||
| `npm run build` | TypeScript check + Vite production build |
|
||||
| `npm run lint` | ESLint for the React app |
|
||||
| `npm run test` | Vitest |
|
||||
| `npm run test` | Vitest unit tests |
|
||||
| `npm run preview` | Preview production build |
|
||||
| `npm run graphql:codegen` | Generate GraphQL types |
|
||||
| `npm run graphql:schema` | Fetch GraphQL schema |
|
||||
| `npm run graphql:codegen` | Generate GraphQL types from schema |
|
||||
| `npm run graphql:schema` | Fetch GraphQL schema from org |
|
||||
|
||||
**Before finishing changes:** run `npm run build` and `npm run lint` from the web app directory; both must succeed.
|
||||
**Before completing any change:** run `npm run build` and `npm run lint` from the UI Bundle directory. Both must pass with zero errors.
|
||||
|
||||
## Agent rules (.a4drules/)
|
||||
## Development conventions
|
||||
|
||||
Markdown rules at the project root under **.a4drules/** define platform constraints:
|
||||
### UI
|
||||
|
||||
- **`.a4drules/webapp-ui.md`** — Salesforce Web Application UI (scaffold with `sf webapp generate`, no LWC/Aura for new UI).
|
||||
- **`.a4drules/webapp-data.md`** — Salesforce data access (Data SDK only, supported APIs, GraphQL workflow, `scripts/graphql-search.sh` for schema lookup).
|
||||
- **Component library:** shadcn/ui primitives in `src/components/ui/`. Always use these over raw HTML equivalents.
|
||||
- **Styling:** Tailwind CSS only. No inline `style={{}}`. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||
- **Icons:** Lucide React.
|
||||
- **Path alias:** `@/*` maps to `src/*`. Use it for all imports.
|
||||
- **TypeScript:** No `any`. Use proper types, generics, or `unknown`.
|
||||
- **Components:** Accept `className?: string` prop. Extract shared state to custom hooks in `src/hooks/`.
|
||||
- **React apps must not** import Salesforce platform modules (`lightning/*`, `@wire`, LWC APIs).
|
||||
|
||||
When rules refer to "web app directory" or `<sfdx-source>/webapplications/<appName>/`, resolve `<sfdx-source>` from `sfdx-project.json` and use the **actual app folder name** for this project.
|
||||
### Routing
|
||||
|
||||
- React Router with `createBrowserRouter`. Route definitions live exclusively in `routes.tsx`.
|
||||
- All page routes are children of the layout route (which renders `appLayout.tsx`).
|
||||
- Default-export one component per page file.
|
||||
- The catch-all `path: '*'` route must always be last.
|
||||
- Navigation uses absolute paths (`/dashboard`). Non-router imports use dot-relative paths (`./utils`).
|
||||
- Navigation visibility is driven by `handle.showInNavigation` on route definitions.
|
||||
|
||||
### Layout and navigation
|
||||
|
||||
- `appLayout.tsx` owns the header, navigation menu, footer, and `<Outlet />`.
|
||||
- To modify header or footer, edit `appLayout.tsx` and create components in `src/components/layout/`.
|
||||
- To add a page, add a route in `routes.tsx` and create the page component — do not modify `appLayout.tsx` or `app.tsx` for page additions.
|
||||
|
||||
### Data access (Salesforce)
|
||||
|
||||
- **All data access uses the Data SDK** (`@salesforce/sdk-data`) via `createDataSDK()`.
|
||||
- **Never** use `fetch()` or `axios` directly for Salesforce data.
|
||||
- **GraphQL is preferred** for record operations (`sdk.graphql`). Use `sdk.fetch` only when GraphQL cannot cover the case (UI API REST, Apex REST, Connect REST, Einstein LLM).
|
||||
- Use optional chaining: `sdk.graphql?.()`, `sdk.fetch?.()`.
|
||||
- Apply the `@optional` directive to all record fields for field-level security resilience.
|
||||
- Verify field and object names via `scripts/graphql-search.sh` before writing queries.
|
||||
- Use `__SF_API_VERSION__` global for API version in REST calls.
|
||||
- **Blocked APIs:** Enterprise REST query endpoint (`/query` with SOQL), `@AuraEnabled` Apex, Chatter API.
|
||||
|
||||
### CSP trusted sites
|
||||
|
||||
Any external domain the app calls (APIs, CDNs, fonts) must have a `.cspTrustedSite-meta.xml` file under `<sfdx-source>/cspTrustedSites/`. Unregistered domains are blocked at runtime. Each subdomain needs its own entry. URLs must be HTTPS with no trailing slash, no path, and no wildcards.
|
||||
|
||||
## Deploying
|
||||
|
||||
**Deployment order:** Metadata (objects, permission sets) must be deployed before GraphQL schema fetch. After any metadata deployment, re-run `npm run graphql:schema` and `npm run graphql:codegen` from the webapp dir. **One-command setup:** `node scripts/setup-cli.mjs --target-org <alias>` runs deploy → permset → schema → codegen in the correct order.
|
||||
**Deployment order matters.** Metadata (objects, permission sets) must be deployed before fetching the GraphQL schema. After any metadata deployment that changes objects, fields, or permissions, re-run schema fetch and codegen.
|
||||
|
||||
From **this project root** (resolve the actual SFDX source path from `sfdx-project.json`):
|
||||
**Recommended sequence:**
|
||||
|
||||
1. Authenticate to the target org
|
||||
2. Build the UI Bundle (`npm run build` in the UI Bundle directory)
|
||||
3. Deploy metadata (`sf project deploy start --source-dir <packageDir> --target-org <alias>`)
|
||||
4. Assign permission sets
|
||||
5. Import data (only with user confirmation)
|
||||
6. Fetch GraphQL schema + run codegen (`npm run graphql:schema && npm run graphql:codegen`)
|
||||
7. Rebuild the UI Bundle (schema changes may affect generated types)
|
||||
|
||||
**Or use the one-command setup:** `node scripts/setup-cli.mjs --target-org <alias>`
|
||||
|
||||
```bash
|
||||
# Build the React app first (replace <sfdx-source> and <appName> with actual values)
|
||||
cd <sfdx-source>/webapplications/<appName> && npm i && npm run build && cd -
|
||||
# Deploy UI Bundle only
|
||||
sf project deploy start --source-dir <sfdx-source>/ui-bundles --target-org <alias>
|
||||
|
||||
# Deploy web app only (replace <sfdx-source> with actual path, e.g. force-app/main/default)
|
||||
sf project deploy start --source-dir <sfdx-source>/webapplications --target-org <alias>
|
||||
|
||||
# Deploy all metadata (use the top-level package directory, e.g. force-app)
|
||||
# Deploy all metadata
|
||||
sf project deploy start --source-dir <packageDir> --target-org <alias>
|
||||
```
|
||||
|
||||
## Conventions (quick reference)
|
||||
## Skills
|
||||
|
||||
- **UI**: shadcn/ui + Tailwind. Import from `@/components/ui/...`.
|
||||
- **Entry**: Keep `App.tsx` and routes in `src/`; add features as new routes or sections, don't replace the app shell but you may modify it to match the requested design.
|
||||
- **Data (Salesforce)**: Follow `.a4drules/webapp-data.md` for all Salesforce data access. Use the Data SDK (`createDataSDK()` + `sdk.graphql` or `sdk.fetch`) — never use `fetch` or `axios` directly. GraphQL is preferred; use `sdk.fetch` when GraphQL is not sufficient.
|
||||
Check for available skills before implementing any of the following:
|
||||
|
||||
| Area | When to consult |
|
||||
|------|----------------|
|
||||
| UI generation | Building pages, components, modifying header/footer/layout |
|
||||
| Salesforce data access | Reading/writing records, GraphQL queries, REST calls |
|
||||
| Metadata and deployment | Scaffolding apps, configuring CSP, deployment sequencing |
|
||||
| Feature installation | Before building something from scratch — check if a pre-built feature exists |
|
||||
| File upload | Adding file upload with Salesforce ContentVersion |
|
||||
| Agentforce conversation | Adding or modifying the Agentforce chat widget |
|
||||
|
||||
Skills are the authoritative source for detailed patterns, constraints, and code examples in each area. This file provides project-level orientation; skills provide implementation depth.
|
||||
|
||||
@ -1,43 +1,43 @@
|
||||
# Property Management App
|
||||
|
||||
A property management sample React web app for the Salesforce platform. Demonstrates property management, maintenance requests, tenant applications, a dashboard, and an Agentforce conversation client. Built with React, Vite, TypeScript, and Tailwind/shadcn.
|
||||
A property management sample React UI Bundle for the Salesforce platform. Demonstrates property management, maintenance requests, tenant applications, a dashboard, and an Agentforce conversation client. Built with React, Vite, TypeScript, and Tailwind/shadcn.
|
||||
|
||||
## What's included
|
||||
|
||||
| Path | Description |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `force-app/main/default/webapplications/propertymanagementapp/` | React web app (source, config, tests) |
|
||||
| `force-app/main/default/objects/` | 17 custom objects — Agent\_\_c, Application\_\_c, KPI_Snapshot\_\_c, Lease\_\_c, Maintenance_Request\_\_c, Maintenance_Worker\_\_c, Notification\_\_c, Payment\_\_c, Property\_\_c, Property_Cost\_\_c, Property_Feature\_\_c, Property_Image\_\_c, Property_Listing\_\_c, Property_Management_Company\_\_c, Property_Owner\_\_c, Property_Sale\_\_c, Tenant\_\_c |
|
||||
| `force-app/main/default/layouts/` | Page layouts for each custom object |
|
||||
| `force-app/main/default/permissionsets/` | `Property_Management_Access` permission set |
|
||||
| `force-app/main/default/data/` | Sample data (JSON) for all objects, importable via `sf data import tree` |
|
||||
| Path | Description |
|
||||
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `force-app/main/default/uiBundles/propertymanagementapp/` | React UI Bundle (source, config, tests) |
|
||||
| `force-app/main/default/objects/` | 17 custom objects — Agent\_\_c, Application\_\_c, KPI_Snapshot\_\_c, Lease\_\_c, Maintenance_Request\_\_c, Maintenance_Worker\_\_c, Notification\_\_c, Payment\_\_c, Property\_\_c, Property_Cost\_\_c, Property_Feature\_\_c, Property_Image\_\_c, Property_Listing\_\_c, Property_Management_Company\_\_c, Property_Owner\_\_c, Property_Sale\_\_c, Tenant\_\_c |
|
||||
| `force-app/main/default/layouts/` | Page layouts for each custom object |
|
||||
| `force-app/main/default/permissionsets/` | `Property_Management_Access` permission set |
|
||||
| `force-app/main/default/data/` | Sample data (JSON) for all objects, importable via `sf data import tree` |
|
||||
|
||||
## Getting started
|
||||
|
||||
Navigate to the web app and install dependencies:
|
||||
Navigate to the UI Bundle and install dependencies:
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/propertymanagementapp
|
||||
cd force-app/main/default/uiBundles/propertymanagementapp
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Opens at http://localhost:5173 by default. For build and test instructions, see the [web app README](force-app/main/default/webapplications/propertymanagementapp/README.md).
|
||||
Opens at http://localhost:5173 by default. For build and test instructions, see the [UI Bundle README](force-app/main/default/uiBundles/propertymanagementapp/README.md).
|
||||
|
||||
## Deploy
|
||||
|
||||
### Deploy everything (metadata + web app)
|
||||
### Deploy everything (metadata + UI Bundle)
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/propertymanagementapp && npm install && npm run build && cd -
|
||||
cd force-app/main/default/uiBundles/propertymanagementapp && npm install && npm run build && cd -
|
||||
sf project deploy start --source-dir force-app --target-org <alias>
|
||||
```
|
||||
|
||||
### Deploy the web app only
|
||||
### Deploy the UI Bundle only
|
||||
|
||||
```bash
|
||||
cd force-app/main/default/webapplications/propertymanagementapp && npm install && npm run build && cd -
|
||||
sf project deploy start --source-dir force-app/main/default/webapplications --target-org <alias>
|
||||
cd force-app/main/default/uiBundles/propertymanagementapp && npm install && npm run build && cd -
|
||||
sf project deploy start --source-dir force-app/main/default/ui-bundles --target-org <alias>
|
||||
```
|
||||
|
||||
### Deploy metadata only (objects, layouts, permission sets)
|
||||
@ -70,7 +70,7 @@ sf data import tree --plan force-app/main/default/data/data-plan.json --target-o
|
||||
|
||||
## Using setup-cli.mjs
|
||||
|
||||
When this app is built (e.g. from the monorepo or from a published package), a `setup-cli.mjs` script is included at the project root. It runs the full setup in one go: login (if needed), deploy metadata, assign the `Property_Management_Access` permission set, prepare and import sample data, fetch GraphQL schema and run codegen, build the web app, and optionally start the dev server.
|
||||
When this app is built (e.g. from the monorepo or from a published package), a `setup-cli.mjs` script is included at the project root. It runs the full setup in one go: login (if needed), deploy metadata, assign the `Property_Management_Access` permission set, prepare and import sample data, fetch GraphQL schema and run codegen, build the UI Bundle, and optionally start the dev server.
|
||||
|
||||
Run from the **project root** (the directory that contains `force-app/`, `sfdx-project.json`, and `setup-cli.mjs`):
|
||||
|
||||
@ -80,15 +80,15 @@ node setup-cli.mjs --target-org <alias>
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | ---------------------------------------------------------------- |
|
||||
| `--skip-login` | Skip browser login (org already authenticated) |
|
||||
| `--skip-data` | Skip data preparation and import |
|
||||
| `--skip-graphql` | Skip GraphQL schema fetch and codegen |
|
||||
| `--skip-webapp-build` | Skip `npm install` and web app build |
|
||||
| `--skip-dev` | Do not start the dev server at the end |
|
||||
| `--permset <name>` | Permission set to assign (default: `Property_Management_Access`) |
|
||||
| `--app <name>` | Web app folder name when multiple exist |
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------- |
|
||||
| `--skip-login` | Skip browser login (org already authenticated) |
|
||||
| `--skip-data` | Skip data preparation and import |
|
||||
| `--skip-graphql` | Skip GraphQL schema fetch and codegen |
|
||||
| `--skip-ui-bundle-build` | Skip `npm install` and UI Bundle build |
|
||||
| `--skip-dev` | Do not start the dev server at the end |
|
||||
| `--permset <name>` | Permission set to assign (default: `Property_Management_Access`) |
|
||||
| `--app <name>` | Web app folder name when multiple exist |
|
||||
|
||||
For all options: `node setup-cli.mjs --help`.
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
public with sharing class MaintenanceRequestTriggerHandler {
|
||||
public without sharing class MaintenanceRequestTriggerHandler {
|
||||
|
||||
/**
|
||||
* Handles before insert logic for Maintenance Request records
|
||||
@ -7,11 +7,14 @@ public with sharing class MaintenanceRequestTriggerHandler {
|
||||
public static void handleBeforeInsert(List<Maintenance_Request__c> newRequests) {
|
||||
// Map to store request type to worker type mappings
|
||||
Map<String, String> requestTypeToWorkerType = new Map<String, String>{
|
||||
'Plumbing' => 'Plumbing',
|
||||
'Electrical' => 'Electrical',
|
||||
'HVAC' => 'HVAC (Heating & Cooling)',
|
||||
'Appliance' => 'Appliance Repair',
|
||||
'Pest' => 'Pest Control'
|
||||
'Plumbing' => 'Plumbing',
|
||||
'Electrical' => 'Electrical',
|
||||
'HVAC' => 'HVAC (Heating & Cooling)',
|
||||
'Appliance' => 'Appliance Repair',
|
||||
'Carpentry' => 'General Carpentry',
|
||||
'Landscaping' => 'Landscaping / Grounds',
|
||||
'Cleaning' => 'Janitorial / Cleaning',
|
||||
'Pest' => 'Pest Control'
|
||||
};
|
||||
|
||||
// Collect unique worker types needed
|
||||
|
||||
@ -29,7 +29,15 @@ public with sharing class TenantTriggerHandler {
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
assignTenantMaintenanceAccessAsync(new List<Id>(userIds));
|
||||
}
|
||||
|
||||
@future
|
||||
private static void assignTenantMaintenanceAccessAsync(List<Id> userIdsList) {
|
||||
Set<Id> userIds = new Set<Id>(userIdsList);
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<PermissionSet> permSets = [
|
||||
SELECT Id
|
||||
FROM PermissionSet
|
||||
|
||||
@ -73,7 +73,9 @@ private class TenantTriggerHandler_Test {
|
||||
static void testNoDuplicateAssign() {
|
||||
Id runAsUserId = UserInfo.getUserId();
|
||||
Tenant__c tenant = new Tenant__c(User__c = runAsUserId);
|
||||
Test.startTest();
|
||||
insert tenant;
|
||||
Test.stopTest();
|
||||
|
||||
Integer countBefore = [
|
||||
SELECT COUNT()
|
||||
@ -82,8 +84,10 @@ private class TenantTriggerHandler_Test {
|
||||
AND PermissionSet.Name = 'Tenant_Maintenance_Access'
|
||||
];
|
||||
|
||||
Test.startTest();
|
||||
tenant.Status__c = 'Active';
|
||||
update tenant;
|
||||
Test.stopTest();
|
||||
|
||||
Integer countAfter = [
|
||||
SELECT COUNT()
|
||||
|
||||
@ -32,11 +32,31 @@
|
||||
<default>false</default>
|
||||
<label>Appliance</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Carpentry</fullName>
|
||||
<default>false</default>
|
||||
<label>Carpentry</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Landscaping</fullName>
|
||||
<default>false</default>
|
||||
<label>Landscaping</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Cleaning</fullName>
|
||||
<default>false</default>
|
||||
<label>Cleaning</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Pest</fullName>
|
||||
<default>false</default>
|
||||
<label>Pest Control</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Other</fullName>
|
||||
<default>false</default>
|
||||
<label>Other</label>
|
||||
</value>
|
||||
</valueSetDefinition>
|
||||
</valueSet>
|
||||
</CustomField>
|
||||
|
||||
@ -7,4 +7,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
### Features
|
||||
|
||||
- auto bump base react app versions and fix issue with base webapplication json ([#175](https://github.com/salesforce-experience-platform-emu/webapps/issues/175)) ([048b5a8](https://github.com/salesforce-experience-platform-emu/webapps/commit/048b5a8449c899fc923aeebc3c76bc5bf1c5e0d4))
|
||||
- auto bump base react app versions and fix issue with base ui-bundle json ([#175](https://github.com/salesforce-experience-platform-emu/webapps/issues/175)) ([048b5a8](https://github.com/salesforce-experience-platform-emu/webapps/commit/048b5a8449c899fc923aeebc3c76bc5bf1c5e0d4))
|
||||
@ -1,6 +1,6 @@
|
||||
# Property Management App
|
||||
|
||||
Property management sample React web app on the Salesforce platform. Includes property management, maintenance requests, tenant applications, a dashboard, and an Agentforce conversation client. Built with React, Vite, TypeScript, and Tailwind/shadcn.
|
||||
Property management sample React UI Bundle on the Salesforce platform. Includes property management, maintenance requests, tenant applications, a dashboard, and an Agentforce conversation client. Built with React, Vite, TypeScript, and Tailwind/shadcn.
|
||||
|
||||
For project-level details (metadata, deploy, sample data), see the [project README](../../../../../../README.md).
|
||||
|
||||
@ -24,7 +24,7 @@ Starts the Vite dev server (default: http://localhost:5173).
|
||||
npm run build
|
||||
```
|
||||
|
||||
Writes the production bundle to `dist/` inside the web app folder.
|
||||
Writes the production bundle to `dist/` inside the UI Bundle folder.
|
||||
|
||||
## Test
|
||||
|
||||
@ -18,7 +18,12 @@ const schemaExists = existsSync(schemaPath);
|
||||
const config = [
|
||||
// Global ignores
|
||||
{
|
||||
ignores: ['build/**/*', 'dist/**/*', 'coverage/**/*'],
|
||||
ignores: [
|
||||
'build/**/*',
|
||||
'dist/**/*',
|
||||
'coverage/**/*',
|
||||
'src/api/graphql-operations-types.ts',
|
||||
],
|
||||
},
|
||||
// Config files and build tools (first to avoid inheritance)
|
||||
{
|
||||
@ -89,11 +94,17 @@ const config = [
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_' },
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
@ -15,8 +15,8 @@
|
||||
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@salesforce/sdk-data": "^1.116.6",
|
||||
"@salesforce/webapp-experimental": "^1.116.6",
|
||||
"@salesforce/sdk-data": "file:../../../../../../../../../sdk/sdk-data",
|
||||
"@salesforce/ui-bundle": "file:../../../../../../../../../ui-bundle",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -32,7 +32,7 @@
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"@salesforce/agentforce-conversation-client": "^1.102.0",
|
||||
"@salesforce/agentforce-conversation-client": "^1.116.9",
|
||||
"recharts": "^2.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -43,7 +43,7 @@
|
||||
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
||||
"@graphql-tools/utils": "^11.0.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@salesforce/vite-plugin-webapp-experimental": "^1.116.6",
|
||||
"@salesforce/vite-plugin-ui-bundle": "file:../../../../../../../../../vite-plugin-ui-bundle",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<WebApplication xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<UIBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<masterLabel>propertymanagementapp</masterLabel>
|
||||
<description>A Salesforce web application.</description>
|
||||
<description>A Salesforce UI Bundle.</description>
|
||||
<isActive>true</isActive>
|
||||
<version>1</version>
|
||||
</WebApplication>
|
||||
</UIBundle>
|
||||
@ -10,7 +10,7 @@
|
||||
*/
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { getOrgInfo } from '@salesforce/webapp-experimental/app';
|
||||
import { getOrgInfo } from '@salesforce/ui-bundle/app';
|
||||
import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql';
|
||||
import { pruneSchema } from '@graphql-tools/utils';
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
import GET_APPLICATIONS from "./query/getApplications.graphql?raw";
|
||||
import UPDATE_APPLICATION_STATUS from "./query/updateApplicationStatus.graphql?raw";
|
||||
import APPLICATION_FOR_APPROVAL_QUERY from "./query/applicationForApproval.graphql?raw";
|
||||
import USER_BY_CONTACT_QUERY from "./query/userByContact.graphql?raw";
|
||||
import EXISTING_TENANT_QUERY from "./query/existingTenant.graphql?raw";
|
||||
import { createRecord } from "@salesforce/ui-bundle/api";
|
||||
import type {
|
||||
GetApplicationsQuery,
|
||||
GetApplicationsQueryVariables,
|
||||
UpdateApplicationStatusMutation,
|
||||
UpdateApplicationStatusMutationVariables,
|
||||
ApplicationForApprovalQuery,
|
||||
ApplicationForApprovalQueryVariables,
|
||||
UserByContactQuery,
|
||||
UserByContactQueryVariables,
|
||||
ExistingTenantQuery,
|
||||
ExistingTenantQueryVariables,
|
||||
} from "../graphql-operations-types.js";
|
||||
import { executeGraphQL } from "../graphqlClient.js";
|
||||
|
||||
export type ApplicationNode = NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<GetApplicationsQuery["uiapi"]["query"]["Application__c"]>["edges"]
|
||||
>[number]
|
||||
>["node"];
|
||||
|
||||
const TENANT_OBJECT_API_NAME = "Tenant__c";
|
||||
|
||||
export async function getApplications(): Promise<NonNullable<ApplicationNode>[]> {
|
||||
try {
|
||||
const data = await executeGraphQL<GetApplicationsQuery, GetApplicationsQueryVariables>(
|
||||
GET_APPLICATIONS,
|
||||
{},
|
||||
);
|
||||
const edges = data?.uiapi?.query?.Application__c?.edges || [];
|
||||
return edges
|
||||
.map((edge) => edge?.node)
|
||||
.filter((node): node is NonNullable<ApplicationNode> => node != null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching applications:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApplicationStatus(
|
||||
applicationId: string,
|
||||
status: string,
|
||||
): Promise<boolean> {
|
||||
const normalizedStatus = status.trim().toLowerCase();
|
||||
if (normalizedStatus === "approved") {
|
||||
await ensureTenantForApprovedApplication(applicationId);
|
||||
}
|
||||
|
||||
const variables: UpdateApplicationStatusMutationVariables = {
|
||||
input: {
|
||||
Id: applicationId,
|
||||
Application__c: {
|
||||
Status__c: status,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const data = await executeGraphQL<
|
||||
UpdateApplicationStatusMutation,
|
||||
UpdateApplicationStatusMutationVariables
|
||||
>(UPDATE_APPLICATION_STATUS, variables);
|
||||
return !!data?.uiapi?.Application__cUpdate?.success;
|
||||
} catch (error) {
|
||||
console.error("Error updating application status:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTenantForApprovedApplication(applicationId: string): Promise<void> {
|
||||
const appData = await executeGraphQL<
|
||||
ApplicationForApprovalQuery,
|
||||
ApplicationForApprovalQueryVariables
|
||||
>(APPLICATION_FOR_APPROVAL_QUERY, { applicationId });
|
||||
const applicationNode = appData.uiapi?.query?.Application__c?.edges?.[0]?.node;
|
||||
if (!applicationNode) {
|
||||
throw new Error("Application record not found.");
|
||||
}
|
||||
|
||||
const contactId = applicationNode.User__c?.value ?? null;
|
||||
const propertyId = applicationNode.Property__c?.value ?? null;
|
||||
const startDate = applicationNode.Start_Date__c?.value ?? null;
|
||||
|
||||
if (!contactId || !propertyId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = await executeGraphQL<UserByContactQuery, UserByContactQueryVariables>(
|
||||
USER_BY_CONTACT_QUERY,
|
||||
{ contactId },
|
||||
);
|
||||
const userId = userData.uiapi?.query?.User?.edges?.[0]?.node?.Id ?? null;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingTenantData = await executeGraphQL<
|
||||
ExistingTenantQuery,
|
||||
ExistingTenantQueryVariables
|
||||
>(EXISTING_TENANT_QUERY, {
|
||||
userId,
|
||||
propertyId,
|
||||
});
|
||||
const existingTenantId = existingTenantData.uiapi?.query?.Tenant__c?.edges?.[0]?.node?.Id ?? null;
|
||||
if (existingTenantId) return;
|
||||
|
||||
const tenantFields: Record<string, unknown> = {
|
||||
User__c: userId,
|
||||
Property__c: propertyId,
|
||||
User_Status__c: "Tenant",
|
||||
Status__c: "Active",
|
||||
};
|
||||
if (startDate) {
|
||||
tenantFields.Start_Date__c = String(startDate);
|
||||
}
|
||||
|
||||
await createRecord(TENANT_OBJECT_API_NAME, tenantFields);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
query ApplicationForApproval($applicationId: ID!) {
|
||||
uiapi {
|
||||
query {
|
||||
Application__c(where: { Id: { eq: $applicationId } }, first: 1) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
User__c @optional {
|
||||
value
|
||||
}
|
||||
Property__c @optional {
|
||||
value
|
||||
}
|
||||
Start_Date__c @optional {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
query ExistingTenant($userId: ID!, $propertyId: ID!) {
|
||||
uiapi {
|
||||
query {
|
||||
Tenant__c(
|
||||
where: { and: [{ User__c: { eq: $userId } }, { Property__c: { eq: $propertyId } }] }
|
||||
first: 1
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
query UserByContact($contactId: ID!) {
|
||||
uiapi {
|
||||
query {
|
||||
User(where: { ContactId: { eq: $contactId } }, first: 1) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 408 B |
|
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 853 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 853 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 550 B After Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
@ -57,6 +57,7 @@ export function AgentforceConversationClient({
|
||||
agentId,
|
||||
inline: inlineProp,
|
||||
headerEnabled,
|
||||
showHeaderIcon,
|
||||
width,
|
||||
height,
|
||||
styleTokens,
|
||||
@ -68,6 +69,8 @@ export function AgentforceConversationClient({
|
||||
const renderingConfig: NonNullable<AgentforceClientConfig["renderingConfig"]> = {
|
||||
mode: inlineProp ? "inline" : "floating",
|
||||
...(headerEnabled !== undefined && { headerEnabled }),
|
||||
...(showHeaderIcon !== undefined && { showHeaderIcon }),
|
||||
...{ showAvatar: false },
|
||||
...(width !== undefined && { width }),
|
||||
...(height !== undefined && { height }),
|
||||
};
|
||||
@ -77,7 +80,7 @@ export function AgentforceConversationClient({
|
||||
...(styleTokens !== undefined && { styleTokens }),
|
||||
renderingConfig,
|
||||
};
|
||||
}, [agentId, inlineProp, headerEnabled, width, height, styleTokens]);
|
||||
}, [agentId, inlineProp, headerEnabled, showHeaderIcon, width, height, styleTokens]);
|
||||
|
||||
const inline = normalizedAgentforceClientConfig?.renderingConfig?.mode === "inline";
|
||||
|
||||
@ -85,7 +88,7 @@ export function AgentforceConversationClient({
|
||||
if (!normalizedAgentforceClientConfig?.agentId) {
|
||||
throw new Error(
|
||||
"AgentforceConversationClient requires agentId. " +
|
||||
"Pass flat props only (agentId, inline, headerEnabled, width, height, styleTokens).",
|
||||
"Pass flat props only (agentId, inline, headerEnabled, showHeaderIcon, width, height, styleTokens).",
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export function FilterRow({
|
||||
}: FilterRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-white rounded-lg shadow-sm border border-gray-200 p-4", className)}
|
||||
className={cn("bg-white rounded-lg shadow-sm border border-gray-200 p-4 pb-2", className)}
|
||||
role="region"
|
||||
aria-label={ariaLabel}
|
||||
{...props}
|
||||