@salesforce/afv-skills 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/skills/creating-webapp/SKILL.md +0 -2
- package/skills/generating-apex/SKILL.md +253 -0
- package/skills/generating-apex/assets/abstract.cls +128 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/SKILL.md +108 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +124 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +112 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +165 -0
- package/skills/generating-apex-test/references/async-testing.md +276 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +219 -0
- package/skills/generating-apex-test/references/test-data-factory.md +176 -0
- package/skills/generating-experience-react-site/SKILL.md +11 -0
- package/skills/generating-flexipage/SKILL.md +39 -57
- package/skills/searching-media/SKILL.md +342 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Mocking Patterns
|
|
2
|
+
|
|
3
|
+
## HTTP Callout Mocking
|
|
4
|
+
|
|
5
|
+
Apex doesn't allow real HTTP callouts in tests. Use `HttpCalloutMock` interface.
|
|
6
|
+
|
|
7
|
+
### Basic Mock Implementation
|
|
8
|
+
|
|
9
|
+
```apex
|
|
10
|
+
@isTest
|
|
11
|
+
public class MockHttpResponse implements HttpCalloutMock {
|
|
12
|
+
|
|
13
|
+
private Integer statusCode;
|
|
14
|
+
private String body;
|
|
15
|
+
|
|
16
|
+
public MockHttpResponse(Integer statusCode, String body) {
|
|
17
|
+
this.statusCode = statusCode;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public HTTPResponse respond(HTTPRequest req) {
|
|
22
|
+
HttpResponse res = new HttpResponse();
|
|
23
|
+
res.setStatusCode(statusCode);
|
|
24
|
+
res.setBody(body);
|
|
25
|
+
res.setHeader('Content-Type', 'application/json');
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Using the Mock
|
|
32
|
+
|
|
33
|
+
```apex
|
|
34
|
+
@isTest
|
|
35
|
+
static void shouldProcessApiResponse_WhenCalloutSucceeds() {
|
|
36
|
+
// Given
|
|
37
|
+
String mockResponse = '{"status": "success", "data": [{"id": "123"}]}';
|
|
38
|
+
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockResponse));
|
|
39
|
+
|
|
40
|
+
// When
|
|
41
|
+
Test.startTest();
|
|
42
|
+
List<ExternalRecord> results = MyIntegrationService.fetchRecords();
|
|
43
|
+
Test.stopTest();
|
|
44
|
+
|
|
45
|
+
// Then
|
|
46
|
+
System.assertEquals(1, results.size(), 'Should parse one record from response');
|
|
47
|
+
System.assertEquals('123', results[0].externalId, 'Should extract correct ID');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@isTest
|
|
51
|
+
static void shouldHandleError_WhenCalloutFails() {
|
|
52
|
+
// Given
|
|
53
|
+
String errorResponse = '{"error": "Unauthorized"}';
|
|
54
|
+
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(401, errorResponse));
|
|
55
|
+
|
|
56
|
+
// When
|
|
57
|
+
Test.startTest();
|
|
58
|
+
CalloutResult result = MyIntegrationService.fetchRecords();
|
|
59
|
+
Test.stopTest();
|
|
60
|
+
|
|
61
|
+
// Then
|
|
62
|
+
System.assertEquals(false, result.isSuccess, 'Should indicate failure');
|
|
63
|
+
System.assert(result.errorMessage.contains('Unauthorized'), 'Should capture error');
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Multi-Request Mock
|
|
68
|
+
|
|
69
|
+
For services making multiple callouts:
|
|
70
|
+
|
|
71
|
+
```apex
|
|
72
|
+
@isTest
|
|
73
|
+
public class MultiRequestMock implements HttpCalloutMock {
|
|
74
|
+
|
|
75
|
+
private Map<String, HttpResponse> endpointResponses;
|
|
76
|
+
|
|
77
|
+
public MultiRequestMock(Map<String, HttpResponse> responses) {
|
|
78
|
+
this.endpointResponses = responses;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public HTTPResponse respond(HTTPRequest req) {
|
|
82
|
+
String endpoint = req.getEndpoint();
|
|
83
|
+
|
|
84
|
+
for (String key : endpointResponses.keySet()) {
|
|
85
|
+
if (endpoint.contains(key)) {
|
|
86
|
+
return endpointResponses.get(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Default 404 if no match
|
|
91
|
+
HttpResponse res = new HttpResponse();
|
|
92
|
+
res.setStatusCode(404);
|
|
93
|
+
res.setBody('{"error": "Not found"}');
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Usage:
|
|
99
|
+
Map<String, HttpResponse> mocks = new Map<String, HttpResponse>();
|
|
100
|
+
|
|
101
|
+
HttpResponse authResponse = new HttpResponse();
|
|
102
|
+
authResponse.setStatusCode(200);
|
|
103
|
+
authResponse.setBody('{"token": "abc123"}');
|
|
104
|
+
mocks.put('/oauth/token', authResponse);
|
|
105
|
+
|
|
106
|
+
HttpResponse dataResponse = new HttpResponse();
|
|
107
|
+
dataResponse.setStatusCode(200);
|
|
108
|
+
dataResponse.setBody('{"records": []}');
|
|
109
|
+
mocks.put('/api/records', dataResponse);
|
|
110
|
+
|
|
111
|
+
Test.setMock(HttpCalloutMock.class, new MultiRequestMock(mocks));
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## StaticResourceCalloutMock
|
|
115
|
+
|
|
116
|
+
For complex response bodies, store JSON in Static Resources:
|
|
117
|
+
|
|
118
|
+
```apex
|
|
119
|
+
@isTest
|
|
120
|
+
static void shouldParseComplexResponse() {
|
|
121
|
+
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
|
|
122
|
+
mock.setStaticResource('TestApiResponse'); // Static Resource name
|
|
123
|
+
mock.setStatusCode(200);
|
|
124
|
+
mock.setHeader('Content-Type', 'application/json');
|
|
125
|
+
|
|
126
|
+
Test.setMock(HttpCalloutMock.class, mock);
|
|
127
|
+
|
|
128
|
+
Test.startTest();
|
|
129
|
+
Result r = MyService.callExternalApi();
|
|
130
|
+
Test.stopTest();
|
|
131
|
+
|
|
132
|
+
System.assertNotEquals(null, r, 'Should parse response');
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Stub API (Enterprise Pattern)
|
|
137
|
+
|
|
138
|
+
For mocking Apex class dependencies using `System.StubProvider`:
|
|
139
|
+
|
|
140
|
+
```apex
|
|
141
|
+
@isTest
|
|
142
|
+
public class MyServiceMock implements System.StubProvider {
|
|
143
|
+
|
|
144
|
+
public Object handleMethodCall(
|
|
145
|
+
Object stubbedObject,
|
|
146
|
+
String stubbedMethodName,
|
|
147
|
+
Type returnType,
|
|
148
|
+
List<Type> paramTypes,
|
|
149
|
+
List<String> paramNames,
|
|
150
|
+
List<Object> args
|
|
151
|
+
) {
|
|
152
|
+
if (stubbedMethodName == 'getAccountData') {
|
|
153
|
+
return new AccountData('Mock Account', 'Active');
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Usage in test:
|
|
160
|
+
@isTest
|
|
161
|
+
static void shouldUseAccountData() {
|
|
162
|
+
MyServiceMock mockProvider = new MyServiceMock();
|
|
163
|
+
IMyService mockService = (IMyService)Test.createStub(IMyService.class, mockProvider);
|
|
164
|
+
|
|
165
|
+
// Inject mock into class under test
|
|
166
|
+
MyController controller = new MyController(mockService);
|
|
167
|
+
|
|
168
|
+
Test.startTest();
|
|
169
|
+
String result = controller.displayAccountInfo();
|
|
170
|
+
Test.stopTest();
|
|
171
|
+
|
|
172
|
+
System.assert(result.contains('Mock Account'), 'Should use mocked data');
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Email Mocking
|
|
177
|
+
|
|
178
|
+
Apex sends real emails by default. Use limits to verify:
|
|
179
|
+
|
|
180
|
+
```apex
|
|
181
|
+
@isTest
|
|
182
|
+
static void shouldSendEmail_WhenTriggered() {
|
|
183
|
+
Integer emailsBefore = Limits.getEmailInvocations();
|
|
184
|
+
|
|
185
|
+
Test.startTest();
|
|
186
|
+
MyService.sendNotification(testContact);
|
|
187
|
+
Test.stopTest();
|
|
188
|
+
|
|
189
|
+
// Verify email was queued (not actually sent in tests)
|
|
190
|
+
System.assertEquals(
|
|
191
|
+
emailsBefore + 1,
|
|
192
|
+
Limits.getEmailInvocations(),
|
|
193
|
+
'One email should be sent'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Platform Event Testing
|
|
199
|
+
|
|
200
|
+
```apex
|
|
201
|
+
@isTest
|
|
202
|
+
static void shouldPublishEvent_WhenRecordCreated() {
|
|
203
|
+
Test.startTest();
|
|
204
|
+
|
|
205
|
+
// Enable event delivery in test context
|
|
206
|
+
Test.enableChangeDataCapture();
|
|
207
|
+
|
|
208
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
209
|
+
|
|
210
|
+
// Deliver events
|
|
211
|
+
Test.getEventBus().deliver();
|
|
212
|
+
|
|
213
|
+
Test.stopTest();
|
|
214
|
+
|
|
215
|
+
// Query platform event trigger results
|
|
216
|
+
List<EventLog__c> logs = [SELECT Id FROM EventLog__c WHERE AccountId__c = :acc.Id];
|
|
217
|
+
System.assertEquals(1, logs.size(), 'Event handler should create log record');
|
|
218
|
+
}
|
|
219
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# TestDataFactory Patterns
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
TestDataFactory is a centralized utility class for creating test records with sensible defaults. It ensures consistent test data across all test classes and reduces duplication.
|
|
6
|
+
|
|
7
|
+
## Base Template
|
|
8
|
+
|
|
9
|
+
```apex
|
|
10
|
+
@isTest
|
|
11
|
+
public class TestDataFactory {
|
|
12
|
+
|
|
13
|
+
// ============ ACCOUNTS ============
|
|
14
|
+
|
|
15
|
+
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
|
|
16
|
+
List<Account> accounts = new List<Account>();
|
|
17
|
+
for (Integer i = 0; i < count; i++) {
|
|
18
|
+
accounts.add(new Account(
|
|
19
|
+
Name = 'Test Account ' + i,
|
|
20
|
+
BillingStreet = '123 Test St',
|
|
21
|
+
BillingCity = 'San Francisco',
|
|
22
|
+
BillingState = 'CA',
|
|
23
|
+
BillingPostalCode = '94105',
|
|
24
|
+
BillingCountry = 'USA',
|
|
25
|
+
Industry = 'Technology',
|
|
26
|
+
Type = 'Customer'
|
|
27
|
+
));
|
|
28
|
+
}
|
|
29
|
+
if (doInsert) insert accounts;
|
|
30
|
+
return accounts;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static Account createAccount(Boolean doInsert) {
|
|
34
|
+
return createAccounts(1, doInsert)[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============ CONTACTS ============
|
|
38
|
+
|
|
39
|
+
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
40
|
+
List<Contact> contacts = new List<Contact>();
|
|
41
|
+
Integer index = 0;
|
|
42
|
+
for (Account acc : accounts) {
|
|
43
|
+
for (Integer i = 0; i < countPerAccount; i++) {
|
|
44
|
+
contacts.add(new Contact(
|
|
45
|
+
FirstName = 'Test',
|
|
46
|
+
LastName = 'Contact ' + index,
|
|
47
|
+
Email = 'test.contact' + index + '@example.com',
|
|
48
|
+
Phone = '555-000-' + String.valueOf(index).leftPad(4, '0'),
|
|
49
|
+
AccountId = acc.Id
|
|
50
|
+
));
|
|
51
|
+
index++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (doInsert) insert contacts;
|
|
55
|
+
return contacts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============ OPPORTUNITIES ============
|
|
59
|
+
|
|
60
|
+
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
61
|
+
List<Opportunity> opps = new List<Opportunity>();
|
|
62
|
+
Integer index = 0;
|
|
63
|
+
for (Account acc : accounts) {
|
|
64
|
+
for (Integer i = 0; i < countPerAccount; i++) {
|
|
65
|
+
opps.add(new Opportunity(
|
|
66
|
+
Name = 'Test Opportunity ' + index,
|
|
67
|
+
AccountId = acc.Id,
|
|
68
|
+
StageName = 'Prospecting',
|
|
69
|
+
CloseDate = Date.today().addDays(30),
|
|
70
|
+
Amount = 10000 + (index * 1000)
|
|
71
|
+
));
|
|
72
|
+
index++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (doInsert) insert opps;
|
|
76
|
+
return opps;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============ USERS ============
|
|
80
|
+
|
|
81
|
+
public static User createUser(String profileName, Boolean doInsert) {
|
|
82
|
+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
83
|
+
String uniqueKey = String.valueOf(DateTime.now().getTime());
|
|
84
|
+
|
|
85
|
+
User u = new User(
|
|
86
|
+
FirstName = 'Test',
|
|
87
|
+
LastName = 'User ' + uniqueKey,
|
|
88
|
+
Email = 'testuser' + uniqueKey + '@example.com',
|
|
89
|
+
Username = 'testuser' + uniqueKey + '@example.com.test',
|
|
90
|
+
Alias = 'tuser',
|
|
91
|
+
TimeZoneSidKey = 'America/Los_Angeles',
|
|
92
|
+
LocaleSidKey = 'en_US',
|
|
93
|
+
EmailEncodingKey = 'UTF-8',
|
|
94
|
+
LanguageLocaleKey = 'en_US',
|
|
95
|
+
ProfileId = p.Id
|
|
96
|
+
);
|
|
97
|
+
if (doInsert) insert u;
|
|
98
|
+
return u;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============ CUSTOM OBJECTS ============
|
|
102
|
+
|
|
103
|
+
// Add methods for your custom objects following the same pattern:
|
|
104
|
+
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Field Override Pattern
|
|
109
|
+
|
|
110
|
+
Allow callers to override default values:
|
|
111
|
+
|
|
112
|
+
```apex
|
|
113
|
+
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
|
|
114
|
+
Account acc = new Account(
|
|
115
|
+
Name = 'Test Account',
|
|
116
|
+
Industry = 'Technology'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Apply overrides
|
|
120
|
+
for (String fieldName : fieldOverrides.keySet()) {
|
|
121
|
+
acc.put(fieldName, fieldOverrides.get(fieldName));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (doInsert) insert acc;
|
|
125
|
+
return acc;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Usage:
|
|
129
|
+
Account acc = TestDataFactory.createAccount(new Map<String, Object>{
|
|
130
|
+
'Name' => 'Custom Name',
|
|
131
|
+
'Industry' => 'Healthcare'
|
|
132
|
+
}, true);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Handling Required Fields and Validation Rules
|
|
136
|
+
|
|
137
|
+
```apex
|
|
138
|
+
public static Account createAccountWithRequiredFields(Boolean doInsert) {
|
|
139
|
+
Account acc = new Account(
|
|
140
|
+
Name = 'Test Account',
|
|
141
|
+
// Required custom fields
|
|
142
|
+
External_Id__c = 'EXT-' + String.valueOf(DateTime.now().getTime()),
|
|
143
|
+
// Fields required by validation rules
|
|
144
|
+
Phone = '555-123-4567',
|
|
145
|
+
Website = 'https://example.com'
|
|
146
|
+
);
|
|
147
|
+
if (doInsert) insert acc;
|
|
148
|
+
return acc;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Record Type Support
|
|
153
|
+
|
|
154
|
+
```apex
|
|
155
|
+
public static Account createAccountByRecordType(String recordTypeName, Boolean doInsert) {
|
|
156
|
+
Id recordTypeId = Schema.SObjectType.Account
|
|
157
|
+
.getRecordTypeInfosByDeveloperName()
|
|
158
|
+
.get(recordTypeName)
|
|
159
|
+
.getRecordTypeId();
|
|
160
|
+
|
|
161
|
+
Account acc = new Account(
|
|
162
|
+
Name = 'Test Account',
|
|
163
|
+
RecordTypeId = recordTypeId
|
|
164
|
+
);
|
|
165
|
+
if (doInsert) insert acc;
|
|
166
|
+
return acc;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Best Practices
|
|
171
|
+
|
|
172
|
+
1. **Always include doInsert parameter** - Allows flexibility for tests that need to modify records before insert
|
|
173
|
+
2. **Use unique identifiers** - Include index or timestamp in Name/Email fields to avoid duplicates
|
|
174
|
+
3. **Set all required fields** - Include all fields required by validation rules
|
|
175
|
+
4. **Return the created records** - Enables chaining and further manipulation
|
|
176
|
+
5. **Create bulk methods first** - Single record methods should call bulk methods with count=1
|
|
@@ -51,6 +51,17 @@ Use the default templates in the docs below. Values in `{braces}` are resolved p
|
|
|
51
51
|
| DigitalExperienceBundle | [configure-metadata-digital-experience-bundle.md](docs/configure-metadata-digital-experience-bundle.md) |
|
|
52
52
|
| DigitalExperience (sfdc_cms__site) | [configure-metadata-digital-experience.md](docs/configure-metadata-digital-experience.md) |
|
|
53
53
|
|
|
54
|
+
### Execution Note for Step 3: Load and use the docs
|
|
55
|
+
- Agents MUST read the full contents of each docs/*.md file referenced in Step 3 before attempting to populate metadata fields.
|
|
56
|
+
- Use your platform's file-read tool (for example, `read_file`) to load these files in full, then perform placeholder substitution for values in `{braces}` using the resolved properties from Step 1.
|
|
57
|
+
- Files to load:
|
|
58
|
+
- `docs/configure-metadata-network.md`
|
|
59
|
+
- `docs/configure-metadata-custom-site.md`
|
|
60
|
+
- `docs/configure-metadata-digital-experience-config.md`
|
|
61
|
+
- `docs/configure-metadata-digital-experience-bundle.md`
|
|
62
|
+
- `docs/configure-metadata-digital-experience.md`
|
|
63
|
+
- Read entire file contents, replace placeholders (e.g. `{siteName}`) with the resolved values, then use the expanded templates to populate the metadata XML/JSON content.
|
|
64
|
+
|
|
54
65
|
### Step 4: Resolve Additional Configurations
|
|
55
66
|
Address any extra configurations the user requests. Use the schemas returned by `get_metadata_api_context` in Step 2 to understand each field's purpose, and update only the minimum necessary fields.
|
|
56
67
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generating-flexipage
|
|
3
|
-
description: "Use this skill when users need to create, generate, modify, or validate Salesforce Lightning pages (FlexiPages). Trigger when users mention RecordPage, AppPage, HomePage, Lightning pages, page layouts, adding components to pages, or page customization. Also use when users say things like
|
|
3
|
+
description: "Use this skill when users need to create, generate, modify, or validate Salesforce Lightning pages (FlexiPages). Trigger when users mention RecordPage, AppPage, HomePage, Lightning pages, page layouts, adding components to pages, or page customization. Also use when users say things like 'create a Lightning page', 'add a component to a page', 'customize the record page', 'generate a FlexiPage', or when they're working with FlexiPage XML files and need help with components, regions, or deployment errors. Always use this skill for any FlexiPage-related work, even if they just mention 'page' in the context of Salesforce."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## When to Use This Skill
|
|
@@ -20,6 +20,8 @@ Use this skill when you need to:
|
|
|
20
20
|
|
|
21
21
|
## Overview
|
|
22
22
|
|
|
23
|
+
**CRITICAL: When creating NEW FlexiPages, you MUST ALWAYS start with the CLI template command.** Never create FlexiPage XML from scratch - the CLI provides valid structure, proper regions, and correct component configuration that prevents deployment errors.
|
|
24
|
+
|
|
23
25
|
Generate Lightning pages (RecordPage, AppPage, HomePage) using CLI bootstrapping for component discovery and configuration.
|
|
24
26
|
|
|
25
27
|
---
|
|
@@ -28,6 +30,8 @@ Generate Lightning pages (RecordPage, AppPage, HomePage) using CLI bootstrapping
|
|
|
28
30
|
|
|
29
31
|
### Step 1: Bootstrap with CLI
|
|
30
32
|
|
|
33
|
+
**MANDATORY FOR NEW PAGES: This step is NOT optional.** Always use the CLI template command when creating a new FlexiPage. The CLI generates valid XML structure, proper regions, and correct metadata that prevents common deployment errors. Only skip this step if you're editing an existing FlexiPage file.
|
|
34
|
+
|
|
31
35
|
```bash
|
|
32
36
|
sf template generate flexipage \
|
|
33
37
|
--name <PageName> \
|
|
@@ -39,19 +43,22 @@ sf template generate flexipage \
|
|
|
39
43
|
--output-dir force-app/main/default/flexipages
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
**Template-specific requirements:**
|
|
43
|
-
- **RecordPage**: Requires `--sobject` (e.g., Account, Custom_Object__c)
|
|
44
|
-
- **RecordPage**: Requires `--primary-field` and `--secondary-fields` for dynamic highlights, `--detail-fields` for full record details. Use the most important identifying field as primary, e.g. Name. Use the secondary fields (max 12, recommended 4-6) to show a summary of the record. Use detail fields to show the full details of the record.
|
|
45
|
-
- **AppPage**: No additional requirements
|
|
46
|
-
- **HomePage**: No additional requirements
|
|
47
|
-
|
|
48
46
|
**Note:** If the `sf template generate flexipage` command fails, recommend users upgrade to the latest version of the Salesforce CLI:
|
|
49
47
|
```bash
|
|
50
48
|
npm install -g @salesforce/cli@latest
|
|
51
49
|
```
|
|
52
50
|
|
|
51
|
+
#### **Field Selection Guidelines**
|
|
52
|
+
- **Validate fields exist**: Use MCP tools or describe commands to discover available fields for the object before specifying them in the command
|
|
53
|
+
- **Prefer compound fields**: Use `Name` (not `FirstName`/`LastName`), `BillingAddress` (not `BillingStreet`/`BillingCity`/`BillingState`), `MailingAddress`, etc. when available
|
|
53
54
|
|
|
54
|
-
**
|
|
55
|
+
#### **Template-specific requirements**
|
|
56
|
+
- **RecordPage**: Requires `--sobject` (e.g., Account, Custom_Object__c)
|
|
57
|
+
- **RecordPage**: Requires `--primary-field` and `--secondary-fields` for dynamic highlights, `--detail-fields` for full record details. Use the most important identifying field as primary, e.g. Name. Use the secondary fields (max 12, recommended 4-6) to show a summary of the record. Use detail fields to show the full details of the record.
|
|
58
|
+
- **AppPage**: No additional requirements
|
|
59
|
+
- **HomePage**: No additional requirements
|
|
60
|
+
|
|
61
|
+
#### **What you get**
|
|
55
62
|
- Valid FlexiPage XML with correct structure
|
|
56
63
|
- Pre-configured regions and basic components
|
|
57
64
|
- Proper field references and facet structure
|
|
@@ -59,17 +66,31 @@ npm install -g @salesforce/cli@latest
|
|
|
59
66
|
|
|
60
67
|
### Step 2: Deploy Base Page
|
|
61
68
|
|
|
69
|
+
Run a **dry-run** deployment of the entire project to validate the page and dependencies:
|
|
62
70
|
```bash
|
|
63
|
-
sf project deploy start --
|
|
71
|
+
sf project deploy start --dry-run -d "force-app/main/default" --test-level NoTestRun --wait 10 --json
|
|
64
72
|
```
|
|
65
73
|
|
|
66
|
-
**
|
|
74
|
+
**Critical:** Fix any deployment errors before proceeding. The page must validate successfully.
|
|
75
|
+
|
|
76
|
+
### Step 3: **STOP - No Further Modifications**
|
|
77
|
+
|
|
78
|
+
**MANDATORY: Stop after Step 2. Do not add components or edit the FlexiPage XML.**
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
This applies even if the user requested:
|
|
81
|
+
- Additional components
|
|
82
|
+
- Page customization
|
|
83
|
+
- Component configuration
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
What you CAN do:
|
|
86
|
+
- Suggest what components would be useful
|
|
87
|
+
- Explain what enhancements are possible
|
|
88
|
+
- Document what would need to be added manually
|
|
71
89
|
|
|
72
|
-
|
|
90
|
+
What you CANNOT do:
|
|
91
|
+
- Modify the XML file
|
|
92
|
+
- Add any components
|
|
93
|
+
- Make any enhancements
|
|
73
94
|
|
|
74
95
|
---
|
|
75
96
|
|
|
@@ -215,6 +236,10 @@ Every fieldInstance requires:
|
|
|
215
236
|
|
|
216
237
|
## Common Deployment Errors
|
|
217
238
|
|
|
239
|
+
### "We couldn't retrieve or load the information on the field"
|
|
240
|
+
**Cause:** Invalid field API name - field doesn't exist on the object or has incorrect spelling
|
|
241
|
+
**Fix:** Use MCP tools or describe commands to discover valid fields, then update the field reference (see Field Selection Guidelines)
|
|
242
|
+
|
|
218
243
|
### "Invalid field reference"
|
|
219
244
|
**Cause:** Used `ObjectName.Field` instead of `Record.Field`
|
|
220
245
|
**Fix:** Change to `Record.{FieldApiName}`
|
|
@@ -245,49 +270,6 @@ Every fieldInstance requires:
|
|
|
245
270
|
|
|
246
271
|
---
|
|
247
272
|
|
|
248
|
-
## Incremental Development Pattern
|
|
249
|
-
|
|
250
|
-
**Philosophy:** Deploy small, working increments. Don't build entire complex page at once.
|
|
251
|
-
|
|
252
|
-
**Process:**
|
|
253
|
-
1. **CLI bootstrap** → Deploy base page
|
|
254
|
-
2. **Add one component** → Deploy
|
|
255
|
-
3. **Add another component** → Deploy
|
|
256
|
-
4. **Repeat** until complete
|
|
257
|
-
|
|
258
|
-
**Benefits:**
|
|
259
|
-
- Isolated errors (know exactly what broke)
|
|
260
|
-
- Faster debugging
|
|
261
|
-
- Build confidence with each success
|
|
262
|
-
- Get user feedback early
|
|
263
|
-
|
|
264
|
-
**Anti-pattern:** Building entire complex page → one giant error cascade.
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## Adding Components to Existing FlexiPages
|
|
269
|
-
|
|
270
|
-
### Workflow
|
|
271
|
-
|
|
272
|
-
When user provides an existing FlexiPage file path:
|
|
273
|
-
|
|
274
|
-
1. **Read the file** using native file I/O
|
|
275
|
-
2. **Parse XML** to extract:
|
|
276
|
-
- **ALL existing component identifiers** (search for all `<identifier>` tags)
|
|
277
|
-
- **ALL existing region/facet names** (search for all `<name>` tags in `<flexiPageRegions>`)
|
|
278
|
-
- Available regions (parse from file, don't assume names)
|
|
279
|
-
- Existing facets
|
|
280
|
-
3. **Verify uniqueness** - ensure your new identifiers and names don't conflict with ANY existing ones
|
|
281
|
-
4. **Check if target facet exists** - if adding to a named facet like `detailTabContent` that already exists:
|
|
282
|
-
- **Add new `<itemInstances>` to existing region** (don't create duplicate region)
|
|
283
|
-
- **Insert before the closing `</flexiPageRegions>` tag of that region**
|
|
284
|
-
5. **Generate component XML** (apply all rules from "Critical XML Rules" section)
|
|
285
|
-
6. **Insert** into appropriate region or add itemInstances to existing facet
|
|
286
|
-
7. **Write** modified XML back to file
|
|
287
|
-
8. **Deploy**: `sf project deploy start --source-dir force-app/...`
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
273
|
### Generating Unique Identifiers
|
|
292
274
|
|
|
293
275
|
**CRITICAL: Before generating ANY new identifier or facet name, follow the rules in section 5 of "Critical XML Rules" above.**
|
|
@@ -474,7 +456,7 @@ Identifier Pattern: flexipage_richText or flexipage_richText_{sequence}
|
|
|
474
456
|
## Validation Checklist
|
|
475
457
|
|
|
476
458
|
Before deploying:
|
|
477
|
-
- [ ] Used CLI to bootstrap
|
|
459
|
+
- [ ] **[NEW PAGES ONLY]** Used CLI to bootstrap - NEVER create FlexiPage XML from scratch
|
|
478
460
|
- [ ] **ALL identifiers are unique** - no duplicate `<identifier>` values anywhere in file
|
|
479
461
|
- [ ] **ALL region/facet names are unique** - no duplicate `<name>` values in `<flexiPageRegions>`
|
|
480
462
|
- [ ] **Multiple components in same facet are combined** - ONE region with multiple `<itemInstances>`, NOT separate regions with same name
|