Skip to content

Commit

Permalink
Sm/qa pr 418 (#419)
Browse files Browse the repository at this point in the history
* feat: preview a visualforce page with open command

* Update messages/open.json

Co-authored-by: Juliet Shackell <[email protected]>

* refactor: use const, ternary

* feat: open vf from meta files, error when file doesn't exist

* test: nuts check file existence

* refactor: move shared code out of tests

* fix: temporarily disable async deploy NUTs

Co-authored-by: Fodil <[email protected]>
Co-authored-by: Juliet Shackell <[email protected]>
Co-authored-by: Steve Hetzel <[email protected]>
  • Loading branch information
4 people authored Feb 3, 2022
1 parent e4e2d76 commit 7a3630b
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 21 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sfdx plugins:install [email protected]

## Issues

Please report any issues at https://github.com/forcedotcom/cli/issues
Please report any issues at <https://github.com/forcedotcom/cli/issues>

## Contributing

Expand All @@ -43,7 +43,7 @@ Please report any issues at https://github.com/forcedotcom/cli/issues
### CLA

External contributors will be required to sign a Contributor's License
Agreement. You can do so by going to https://cla.salesforce.com/sign-cla.
Agreement. You can do so by going to <https://cla.salesforce.com/sign-cla>.

### Build

Expand Down Expand Up @@ -947,9 +947,9 @@ DESCRIPTION
EXAMPLES
To deploy the source files in a directory:
$ sfdx force:source:deploy -p path/to/source
$ sfdx force:source:deploy -p path/to/source
To deploy a specific Apex class and the objects whose source is in a directory:
$ sfdx force:source:deploy -p "path/to/apex/classes/MyClass.cls,path/to/source/objects"
$ sfdx force:source:deploy -p "path/to/apex/classes/MyClass.cls,path/to/source/objects"
To deploy source files in a comma-separated list that contains spaces:
$ sfdx force:source:deploy -p "path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, path/to/apex/classes"
To deploy all Apex classes:
Expand Down Expand Up @@ -1186,7 +1186,7 @@ OPTIONS
DESCRIPTION
Opens the specified Lightning Page in Lightning App Builder. Lightning Page files have the suffix .flexipage-meta.xml,
and are stored in the flexipages directory. If you specify a different type of file, this command opens your org’s
and are stored in the flexipages directory. If you specify a visualforce page (with a .page suffix) this command let you preview your visualforce page. If you specify a different type of file, this command opens your org’s
home page.
The file opens in your default browser.
Expand Down
2 changes: 1 addition & 1 deletion messages/open.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "edit a Lightning Page with Lightning App Builder\nOpens the specified Lightning Page in Lightning App Builder. Lightning Page files have the suffix .flexipage-meta.xml, and are stored in the flexipages directory. If you specify a different type of file, this command opens your org’s home page.\n\nThe file opens in your default browser.\nIf no browser-based editor is available for the selected file, this command opens your org's home page.\nTo generate a URL for the browser-based editor but not open the editor, use --urlonly.",
"description": "edit a Lightning Page with Lightning App Builder\nOpens the specified Lightning Page in Lightning App Builder. Lightning Page files have the suffix .flexipage-meta.xml, and are stored in the flexipages directory. \nIf you specify a Visualforce page, which has a .page suffix, the page opens in your browser so you can preview it. If you specify a different type of file, this command opens your org’s home page.\n\nThe file opens in your default browser.\nIf no browser-based editor is available for the selected file, this command opens your org's home page.\nTo generate a URL for the browser-based editor but not open the editor, use --urlonly.",
"examples": [
"$ sfdx force:source:open -f path/to/source",
"$ sfdx force:source:open -r -f path/to/source",
Expand Down
36 changes: 24 additions & 12 deletions src/commands/force/source/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export class Open extends SourceCommand {

private async doOpen(): Promise<void> {
const typeName = this.getTypeNameDefinitionByFileName(path.resolve(this.flags.sourcefile as string));
const openPath = typeName === 'FlexiPage' ? await this.setUpOpenPath() : await this.buildFrontdoorUrl();
const openPath = ['FlexiPage', 'ApexPage'].includes(typeName)
? await this.setUpOpenPath(typeName)
: await this.buildFrontdoorUrl();

this.openResult = await this.open(openPath);
}
Expand All @@ -72,6 +74,7 @@ export class Open extends SourceCommand {
const components: SourceComponent[] = metadataResolver.getComponentsFromPath(fsPath);
return components[0].type.name;
}
throw new SfdxError(`File not found: ${fsPath}`, 'FileNotFound');
}

private async buildFrontdoorUrl(): Promise<string> {
Expand All @@ -98,18 +101,27 @@ export class Open extends SourceCommand {
return this.flags.urlonly ? result : this.openBrowser(url, result);
}

private async setUpOpenPath(): Promise<string> {
private async setUpOpenPath(pageType: string): Promise<string> {
try {
const flexipage = await this.org
.getConnection()
.singleRecordQuery<{ Id: string }>(
`SELECT id FROM flexipage WHERE DeveloperName='${path.basename(
this.flags.sourcefile as string,
'.flexipage-meta.xml'
)}'`,
{ tooling: true }
);
return `/visualEditor/appBuilder.app?pageId=${flexipage.Id}`;
if (pageType === 'FlexiPage') {
const flexipage = await this.org
.getConnection()
.singleRecordQuery<{ Id: string }>(
`SELECT id FROM flexipage WHERE DeveloperName='${path.basename(
this.flags.sourcefile as string,
'.flexipage-meta.xml'
)}'`,
{ tooling: true }
);
return `/visualEditor/appBuilder.app?pageId=${flexipage.Id}`;
} else if (pageType === 'ApexPage') {
return `/apex/${path
.basename(this.flags.sourcefile as string)
.replace('.page-meta.xml', '')
.replace('.page', '')}`;
} else {
return '_ui/flexipage/ui/FlexiPageFilterListPage';
}
} catch (error) {
return '_ui/flexipage/ui/FlexiPageFilterListPage';
}
Expand Down
26 changes: 26 additions & 0 deletions test/commands/source/open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ describe('force:source:open', () => {
},
},
] as SourceComponent[];
const apexPageComponents = [
{
name: 'MyPage',
type: {
id: 'page',
name: 'ApexPage',
suffix: 'page',
directoryName: 'pages',
inFolder: false,
strictDirectoryName: false,
},
},
] as SourceComponent[];
const testIp = '1.1.1.1';
const orgId = '00DJ0000003htplMAA';
const defaultDir = join('my', 'default', 'package');
Expand Down Expand Up @@ -108,6 +121,9 @@ describe('force:source:open', () => {
if (fsPath.includes('layout')) {
return layoutComponents;
}
if (fsPath.includes('page')) {
return apexPageComponents;
}
}
);
stubMethod(sandbox, MyDomainResolver.prototype, 'resolve').resolves(testIp);
Expand Down Expand Up @@ -140,4 +156,14 @@ describe('force:source:open', () => {
url: `${frontDoorUrl}&retURL=${encodeURIComponent(decodeURIComponent(frontDoorUrl))}`,
});
});

it('given a visualforce page url return /apex/pagename', async () => {
const sourcefile = 'force-app/main/default/pages/MyPage.page';
const result = await runOpenCmd(['--sourcefile', sourcefile, '--json', '--urlonly']);
expect(result).to.deep.equal({
orgId,
username,
url: `${frontDoorUrl}&retURL=%2Fapex%2FMyPage`,
});
});
});
2 changes: 1 addition & 1 deletion test/nuts/delete.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { execCmd } from '@salesforce/cli-plugins-testkit';
import { SourceTestkit } from '@salesforce/source-testkit';
import { exec } from 'shelljs';
import { FileResponse } from '@salesforce/source-deploy-retrieve';
import { isNameObsolete } from './deployDestructive.nut';
import { isNameObsolete } from './shared/isNameObsolete';

describe('source:delete NUTs', () => {
const executable = path.join(process.cwd(), 'bin', 'run');
Expand Down
17 changes: 15 additions & 2 deletions test/nuts/open.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
*/

import * as querystring from 'querystring';
import * as path from 'path';
import * as fs from 'fs';
import { expect } from '@salesforce/command/lib/test';
import { TestSession } from '@salesforce/cli-plugins-testkit';
import { execCmd } from '@salesforce/cli-plugins-testkit';
import { AnyJson, Dictionary, getString, isArray } from '@salesforce/ts-types';

const flexiPagePath = 'force-app/main/default/flexipages/Property_Explorer.flexipage-meta.xml';
const layoutDir = 'force-app/main/default/layouts';
const layoutFilePath = path.join(layoutDir, 'MyLayout.layout-meta.xml');

describe('force:source:open', () => {
let session: TestSession;
Expand All @@ -33,6 +37,10 @@ describe('force:source:open', () => {
defaultUsername = getString(session.setup[0], 'result.username');
defaultUserOrgId = getString(session.setup[0], 'result.orgId');
}
await fs.promises.writeFile(
path.join(session.project.dir, layoutFilePath),
'<layout xmlns="http://soap.sforce.com/2006/04/metadata">\n</layout>'
);
});

after(async () => {
Expand All @@ -49,13 +57,18 @@ describe('force:source:open', () => {
expect(result).to.property('url').to.include(querystring.escape('visualEditor/appBuilder.app'));
});
it("should produce the org's frontdoor url when edition of file is not supported", () => {
const unsupportedFilePath = 'force-app/main/default/layouts/MyLayout.layout-meta.xml';
const result = execCmd<Dictionary>(`force:source:open -f ${unsupportedFilePath} --urlonly --json`, {
const result = execCmd<Dictionary>(`force:source:open -f ${layoutFilePath} --urlonly --json`, {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result).to.be.ok;
expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername });
expect(result).to.property('url').to.include(querystring.escape('secur/frontdoor.jsp'));
});
it('should throw for non-existent files', () => {
const result = execCmd<Dictionary>('force:source:open -f NotThere --urlonly', {
ensureExitCode: 1,
});
expect(result.shellOutput.stderr).to.include('NotThere');
});
});
});
20 changes: 20 additions & 0 deletions test/nuts/shared/isNameObsolete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2020, 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 { AuthInfo, Connection } from '@salesforce/core';

export const isNameObsolete = async (username: string, memberType: string, memberName: string): Promise<boolean> => {
const connection = await Connection.create({
authInfo: await AuthInfo.create({ username }),
});

const res = await connection.singleRecordQuery<{ IsNameObsolete: boolean }>(
`SELECT IsNameObsolete FROM SourceMember WHERE MemberType='${memberType}' AND MemberName='${memberName}'`,
{ tooling: true }
);

return res.IsNameObsolete;
};

0 comments on commit 7a3630b

Please sign in to comment.