Skip to content
Open
23 changes: 23 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@
"flags": ["api-version", "event", "flags-dir", "json", "loglevel", "name", "output-dir", "sobject", "template"],
"plugin": "@salesforce/plugin-templates"
},
{
"alias": ["flexipage:generate"],
"command": "template:generate:flexipage",
"flagAliases": ["apiversion", "entity", "entity-name", "flexipagename", "masterlabel", "outputdir"],
"flagChars": ["d", "i", "n", "s", "t"],
"flags": [
"api-version",
"description",
"detail-fields",
"flags-dir",
"internal",
"json",
"label",
"loglevel",
"name",
"output-dir",
"primary-field",
"secondary-fields",
"sobject",
"template"
],
"plugin": "@salesforce/plugin-templates"
},
{
"alias": ["force:visualforce:component:create"],
"command": "visualforce:generate:component",
Expand Down
83 changes: 83 additions & 0 deletions messages/flexipage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# examples

- Generate a RecordPage FlexiPage for the Account object in the current directory:

<%= config.bin %> <%= command.id %> --name Account_Record_Page --template RecordPage --sobject Account

- Generate an AppPage FlexiPage in the "force-app/main/default/flexipages" directory:

<%= config.bin %> <%= command.id %> --name Sales_Dashboard --template AppPage --output-dir force-app/main/default/flexipages

- Generate a HomePage FlexiPage with a custom label:

<%= config.bin %> <%= command.id %> --name Custom_Home --template HomePage --label "Sales Home Page"

- Generate a RecordPage with dynamic highlights and detail fields:

<%= config.bin %> <%= command.id %> --name Property_Page --template RecordPage --sobject Rental_Property__c --primary-field Name --secondary-fields Property_Address__c,City__c --detail-fields Name,Property_Address__c,City__c,Monthly_Rent__c,Bedrooms__c

# summary

Generate a FlexiPage, also known as a Lightning page.

# description

FlexiPages are the metadata types associated with a Lightning page. A Lightning page represents a customizable screen made up of regions containing Lightning components.

You can use this command to generate these types of FlexiPages; specify the type with the --template flag:

- AppPage: A Lightning page used as the home page for a custom app or a standalone application page.
- HomePage: A Lightning page used to override the Home page in Lightning Experience.
- RecordPage: A Lightning page used to override an object record page in Lightning Experience. Requires that you specify the object name with the --sobject flag.

# flags.name.summary

Name of the FlexiPage.

# flags.name.description

The name can contain only alphanumeric characters, must start with a letter, and can't end with an underscore or contain two consecutive underscores.

# flags.template.summary

Template type for the FlexiPage.

# flags.label.summary

Label of this FlexiPage; if not specified, uses the FlexiPage name as the label.

# flags.description.summary

Description for the FlexiPage, which provides context about its purpose.

# flags.sobject.summary

API name of the Salesforce object; required when creating a RecordPage.

# flags.sobject.description

For RecordPage FlexiPages, you must specify the associated object API name, such as 'Account', 'Opportunity', or 'Custom_Object__c'. This sets the `sobjectType` field in the FlexiPage metadata.

# flags.primary-field.summary

Primary field for the dynamic highlights header; typically 'Name'. Used only with RecordPage.

# flags.secondary-fields.summary

Secondary fields shown in the dynamic highlights header. Specify multiple fields separated by commas. Maximum of 11 fields. Used only with RecordPage.

# flags.detail-fields.summary

Fields to display in the Details tab. Specify multiple fields separated by commas. Fields are split into two columns. Used only with RecordPage.

# errors.recordPageRequiresSobject

RecordPage template requires the --sobject flag to specify the Salesforce object API name (e.g., 'Account', 'Opportunity', 'Custom_Object__c').

# errors.tooManySecondaryFields

Too many secondary fields specified (%s). The Dynamic Highlights Panel supports a maximum of %s secondary fields.

# errors.flagRequiresRecordPage

The --%s flag can only be used with --template RecordPage.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
"subtopics": {
"apex": {
"description": "Create an apex class or trigger."
},
"flexipage": {
"description": "Generate a Lightning FlexiPage from a template."
}
}
}
Expand Down
126 changes: 126 additions & 0 deletions src/commands/template/generate/flexipage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand, Ux } from '@salesforce/sf-plugins-core';
import { CreateOutput, FlexipageOptions, TemplateType } from '@salesforce/templates';
import { Messages } from '@salesforce/core';
import { getCustomTemplates, runGenerator } from '../../../utils/templateCommand.js';
import { internalFlag, outputDirFlag } from '../../../utils/flags.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-templates', 'flexipage');

export default class FlexipageGenerate extends SfCommand<CreateOutput> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly aliases = ['flexipage:generate'];
public static readonly deprecateAliases = true;
public static readonly state = 'beta';
public static readonly flags = {
name: Flags.string({
char: 'n',
summary: messages.getMessage('flags.name.summary'),
description: messages.getMessage('flags.name.description'),
required: true,
aliases: ['flexipagename'],
deprecateAliases: true,
}),
template: Flags.option({
char: 't',
summary: messages.getMessage('flags.template.summary'),
required: true,
options: ['RecordPage', 'AppPage', 'HomePage'] as const,
})(),
'output-dir': outputDirFlag,
'api-version': orgApiVersionFlagWithDeprecations,
label: Flags.string({
summary: messages.getMessage('flags.label.summary'),
aliases: ['masterlabel'],
deprecateAliases: true,
}),
description: Flags.string({
summary: messages.getMessage('flags.description.summary'),
}),
sobject: Flags.string({
char: 's',
summary: messages.getMessage('flags.sobject.summary'),
description: messages.getMessage('flags.sobject.description'),
aliases: ['entity-name', 'entity'],
deprecateAliases: true,
}),
'primary-field': Flags.string({
summary: messages.getMessage('flags.primary-field.summary'),
}),
'secondary-fields': Flags.string({
summary: messages.getMessage('flags.secondary-fields.summary'),
multiple: true,
delimiter: ',',
}),
'detail-fields': Flags.string({
summary: messages.getMessage('flags.detail-fields.summary'),
multiple: true,
delimiter: ',',
}),
internal: internalFlag,
loglevel,
};

private static readonly MAX_SECONDARY_FIELDS = 11;

public async run(): Promise<CreateOutput> {
const { flags } = await this.parse(FlexipageGenerate);

// Validate RecordPage requires sobject
if (flags.template === 'RecordPage' && !flags.sobject) {
throw new Error(messages.getMessage('errors.recordPageRequiresSobject'));
}

// Validate RecordPage-specific flags are only used with RecordPage template
const recordPageOnlyFlags = ['primary-field', 'secondary-fields', 'detail-fields'] as const;
if (flags.template !== 'RecordPage') {
for (const flagName of recordPageOnlyFlags) {
if (flags[flagName]) {
throw new Error(messages.getMessage('errors.flagRequiresRecordPage', [flagName]));
}
}
}

// Validate secondary fields limit (Dynamic Highlights Panel supports max 11)
const secondaryFieldsCount = flags['secondary-fields']?.length ?? 0;
if (secondaryFieldsCount > FlexipageGenerate.MAX_SECONDARY_FIELDS) {
throw new Error(
messages.getMessage('errors.tooManySecondaryFields', [
secondaryFieldsCount.toString(),
FlexipageGenerate.MAX_SECONDARY_FIELDS.toString(),
])
);
}

// Convert CLI flags to library options
const flagsAsOptions: FlexipageOptions = {
flexipagename: flags.name,
template: flags.template,
outputdir: flags['output-dir'],
apiversion: flags['api-version'],
masterlabel: flags.label,
description: flags.description,
entityName: flags.sobject,
primaryField: flags['primary-field'],
secondaryFields: flags['secondary-fields'] ?? [],
detailFields: flags['detail-fields'] ?? [],
internal: flags.internal,
};

return runGenerator({
templateType: TemplateType.Flexipage,
opts: flagsAsOptions,
ux: new Ux({ jsonEnabled: this.jsonEnabled() }),
templates: getCustomTemplates(this.configAggregator),
});
}
}
109 changes: 109 additions & 0 deletions test/commands/template/generate/flexipage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { TestContext } from '@salesforce/core/testSetup';
import { expect } from 'chai';
import { stubSfCommandUx } from '@salesforce/sf-plugins-core';
import FlexipageGenerate from '../../../../src/commands/template/generate/flexipage.js';

describe('template:generate:flexipage', () => {
const $$ = new TestContext();

beforeEach(() => {
stubSfCommandUx($$.SANDBOX);
});

afterEach(() => {
$$.restore();
});

it('should require name flag', async () => {
try {
await FlexipageGenerate.run([]);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('Missing required flag');
}
});

it('should require template flag', async () => {
try {
await FlexipageGenerate.run(['--name', 'TestPage']);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('Missing required flag');
}
});

it('should require sobject for RecordPage', async () => {
try {
await FlexipageGenerate.run(['--name', 'TestPage', '--template', 'RecordPage']);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('sobject');
}
});

it('should reject more than 11 secondary fields', async () => {
try {
await FlexipageGenerate.run([
'--name',
'TestPage',
'--template',
'RecordPage',
'--sobject',
'Account',
'--secondary-fields',
'F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12',
]);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('Too many secondary fields');
}
});

it('should be marked as beta', () => {
expect(FlexipageGenerate.state).to.equal('beta');
});

it('should reject primary-field with non-RecordPage template', async () => {
try {
await FlexipageGenerate.run(['--name', 'TestPage', '--template', 'AppPage', '--primary-field', 'Name']);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('primary-field');
expect(error.message).to.include('RecordPage');
}
});

it('should reject secondary-fields with non-RecordPage template', async () => {
try {
await FlexipageGenerate.run(['--name', 'TestPage', '--template', 'HomePage', '--secondary-fields', 'Industry']);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('secondary-fields');
expect(error.message).to.include('RecordPage');
}
});

it('should reject detail-fields with non-RecordPage template', async () => {
try {
await FlexipageGenerate.run(['--name', 'TestPage', '--template', 'AppPage', '--detail-fields', 'Name,Phone']);
expect.fail('Should have thrown an error');
} catch (err) {
const error = err as Error;
expect(error.message).to.include('detail-fields');
expect(error.message).to.include('RecordPage');
}
});
});