Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 89 additions & 8 deletions src/client/power-pages/actions-hub/ActionsHubCommandHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { IWebsiteInfo } from './models/IWebsiteInfo';
import moment from 'moment';
import { SiteVisibility } from './models/SiteVisibility';
import { traceError, traceInfo } from './TelemetryHelper';
import { IPowerPagesConfig, IPowerPagesConfigData } from './models/IPowerPagesConfig';

const sortByCreatedOn = <T extends { createdOn?: string | null }>(item1: T, item2: T): number => {
const date1 = new Date(item1.createdOn || '').valueOf(); //NaN if createdOn is null or undefined
Expand Down Expand Up @@ -355,23 +356,103 @@ export const uploadSite = async (siteTreeItem: SiteTreeItem, websitePath: string
}
};

const uploadCodeSite = async (siteInfo: IWebsiteInfo, uploadPath: string) => {
traceInfo(Constants.EventNames.ACTIONS_HUB_UPLOAD_CODE_SITE_CALLED, { methodName: uploadCodeSite.name, siteIdToUpload: siteInfo.websiteId });
/**
* Reads and parses the powerpages.config.json file
* @param configFilePath Path to the configuration file
* @returns Parsed configuration data
*/
const readPowerPagesConfig = (configFilePath: string): IPowerPagesConfigData => {
if (!fs.existsSync(configFilePath)) {
return { hasCompiledPath: false, hasSiteName: false };
}

try {
const compiledPath = await getCompiledOutputFolderPath();
const configContent = fs.readFileSync(configFilePath, UTF8_ENCODING);
const config: IPowerPagesConfig = JSON.parse(configContent);

const hasCompiledPath = Boolean(config?.compiledPath);
const hasSiteName = Boolean(config?.siteName);

return { hasCompiledPath, hasSiteName };
} catch (configError) {
traceError(Constants.EventNames.POWER_PAGES_CONFIG_PARSE_FAILED, configError as Error, { methodName: readPowerPagesConfig.name });
}
return { hasCompiledPath: false, hasSiteName: false };
};

/**
* Builds the upload code site command for pac pages upload-code-site
* @param uploadPath Root path for the upload
* @param siteInfo Site information
* @param configData Configuration data
* @returns The complete upload command as a string
*/
const buildUploadCodeSiteCommand = async (
uploadPath: string,
siteInfo: IWebsiteInfo,
configData: IPowerPagesConfigData
): Promise<string> => {
const commandParts = ["pac", "pages", "upload-code-site"];

commandParts.push("--rootPath", `"${uploadPath}"`);

if (!configData.hasCompiledPath) {
const compiledPath = await getCompiledOutputFolderPath();
if (!compiledPath) {
await vscode.window.showErrorMessage(vscode.l10n.t(Constants.Strings.UPLOAD_CODE_SITE_COMPILED_OUTPUT_FOLDER_NOT_FOUND));
await vscode.window.showErrorMessage(
vscode.l10n.t(Constants.Strings.UPLOAD_CODE_SITE_COMPILED_OUTPUT_FOLDER_NOT_FOUND)
);
return "";
}
commandParts.push("--compiledPath", `"${compiledPath}"`);
}

if (!configData.hasSiteName) {
commandParts.push("--siteName", `"${siteInfo.name}"`);
}

return commandParts.join(" ");
};

/**
* Uploads a Power Pages code site to the environment
* @param siteInfo Information about the site to upload
* @param uploadPath Path to the site folder to upload
*/
const uploadCodeSite = async (siteInfo: IWebsiteInfo, uploadPath: string) => {
traceInfo(Constants.EventNames.ACTIONS_HUB_UPLOAD_CODE_SITE_CALLED, {
methodName: uploadCodeSite.name,
siteIdToUpload: siteInfo.websiteId
});

try {
const configFilePath = path.join(uploadPath, Constants.Strings.POWER_PAGES_CONFIG_FILE_NAME);
const configData = readPowerPagesConfig(configFilePath);

const uploadCommand = await buildUploadCodeSiteCommand(
uploadPath,
siteInfo,
configData
);

if (!uploadCommand) {
return;
}

traceInfo(Constants.EventNames.ACTIONS_HUB_UPLOAD_OTHER_SITE_PAC_TRIGGERED, { methodName: uploadCodeSite.name, siteIdToUpload: siteInfo.websiteId });
PacTerminal.getTerminal().sendText(`pac pages upload-code-site --rootPath "${uploadPath}" --compiledPath "${compiledPath}" --siteName "${siteInfo.name}"`);
traceInfo(Constants.EventNames.ACTIONS_HUB_UPLOAD_OTHER_SITE_PAC_TRIGGERED, {
methodName: uploadCodeSite.name,
siteIdToUpload: siteInfo.websiteId
});

PacTerminal.getTerminal().sendText(uploadCommand);
} catch (error) {
traceError(Constants.EventNames.ACTIONS_HUB_UPLOAD_CODE_SITE_FAILED, error as Error, { methodName: uploadCodeSite.name, siteIdToUpload: siteInfo.websiteId });
traceError(Constants.EventNames.ACTIONS_HUB_UPLOAD_CODE_SITE_FAILED, error as Error, {
methodName: uploadCodeSite.name,
siteIdToUpload: siteInfo.websiteId
});
await vscode.window.showErrorMessage(vscode.l10n.t(Constants.Strings.UPLOAD_CODE_SITE_FAILED));
}
}
};

/**
* Uploads a site that isn't in the current environment
Expand Down
6 changes: 4 additions & 2 deletions src/client/power-pages/actions-hub/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const Constants = {
CONFIGURATION_NAME: "powerPlatform.pages",
DOWNLOAD_SETTING_NAME: "downloadSiteFolder",
UPLOAD_CODE_SITE_COMPILED_OUTPUT_FOLDER_NOT_FOUND: vscode.l10n.t("Please select the folder that contains your compiled output to upload your site."),
UPLOAD_CODE_SITE_FAILED: vscode.l10n.t("Upload failed. Please try again later.")
UPLOAD_CODE_SITE_FAILED: vscode.l10n.t("Upload failed. Please try again later."),
POWER_PAGES_CONFIG_FILE_NAME: "powerpages.config.json"
},
EventNames: {
ACTIONS_HUB_ENABLED: "ActionsHubEnabled",
Expand Down Expand Up @@ -105,7 +106,8 @@ export const Constants = {
ACTIONS_HUB_OPEN_SITE_IN_STUDIO_FAILED: "ActionsHubOpenSiteInStudioFailed",
ACTIONS_HUB_UPLOAD_CODE_SITE_CALLED: "ActionsHubUploadCodeSiteCalled",
ACTIONS_HUB_UPLOAD_CODE_SITE_FAILED: "ActionsHubUploadCodeSiteFailed",
ACTIONS_HUB_UPLOAD_OTHER_CODE_SITE_PAC_TRIGGERED: "ActionsHubUploadOtherCodeSitePacTriggered"
ACTIONS_HUB_UPLOAD_OTHER_CODE_SITE_PAC_TRIGGERED: "ActionsHubUploadOtherCodeSitePacTriggered",
POWER_PAGES_CONFIG_PARSE_FAILED: "PowerPagesConfigParseFailed"
},
FeatureNames: {
REFRESH_ENVIRONMENT: "RefreshEnvironment"
Expand Down
20 changes: 20 additions & 0 deletions src/client/power-pages/actions-hub/models/IPowerPagesConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

/**
* Interface for Power Pages configuration file structure
*/
export interface IPowerPagesConfig {
compiledPath?: string;
siteName?: string;
}

/**
* Interface for parsed configuration data
*/
export interface IPowerPagesConfigData {
hasCompiledPath: boolean;
hasSiteName: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,70 @@ describe('ActionsHubCommandHandlers', () => {
expect(mockSendText.firstCall.args[0]).to.equal(`pac pages upload-code-site --rootPath "test-path" --compiledPath "D:/foo" --siteName "Test Site"`);
});

it('should not upload code site when compiledPath selection is cancelled', async () => {
mockSiteTreeItem = new SiteTreeItem({
name: "Test Site",
websiteId: "test-id",
dataModelVersion: 1,
status: WebsiteStatus.Active,
websiteUrl: 'https://test-site.com',
isCurrent: false,
siteVisibility: SiteVisibility.Private,
siteManagementUrl: "https://inactive-site-1-management.com",
createdOn: "2025-03-20",
creator: "Test Creator",
isCodeSite: true
});

const mockQuickPick = sinon.stub(vscode.window, 'showQuickPick');
mockQuickPick.resolves({ label: "Browse..." });

const mockShowOpenDialog = sinon.stub(vscode.window, 'showOpenDialog');
mockShowOpenDialog.resolves(undefined);

const mockShowErrorMessage = sinon.stub(vscode.window, 'showErrorMessage');

await uploadSite(mockSiteTreeItem, "");

expect(mockQuickPick.calledOnce).to.be.true;
expect(mockShowOpenDialog.calledOnce).to.be.true;
expect(mockShowErrorMessage.calledOnce).to.be.true;
expect(mockSendText.called).to.be.false;
});

it('should handle errors during code site upload', async () => {
mockSiteTreeItem = new SiteTreeItem({
name: "Test Site",
websiteId: "test-id",
dataModelVersion: 1,
status: WebsiteStatus.Active,
websiteUrl: 'https://test-site.com',
isCurrent: false,
siteVisibility: SiteVisibility.Private,
siteManagementUrl: "https://inactive-site-1-management.com",
createdOn: "2025-03-20",
creator: "Test Creator",
isCodeSite: true
});

const mockQuickPick = sinon.stub(vscode.window, 'showQuickPick');
mockQuickPick.resolves({ label: "Browse..." });

const mockShowOpenDialog = sinon.stub(vscode.window, 'showOpenDialog');
mockShowOpenDialog.resolves([{ fsPath: "D:/foo" } as unknown as vscode.Uri]);

const mockShowErrorMessage = sinon.stub(vscode.window, 'showErrorMessage');
mockSendText.throws(new Error('Upload code site failed'));

await uploadSite(mockSiteTreeItem, "");

expect(mockQuickPick.calledOnce).to.be.true;
expect(mockShowOpenDialog.calledOnce).to.be.true;
expect(traceErrorStub.calledOnce).to.be.true;
expect(traceErrorStub.firstCall.args[0]).to.equal(Constants.EventNames.ACTIONS_HUB_UPLOAD_CODE_SITE_FAILED);
expect(mockShowErrorMessage.calledOnce).to.be.true;
});

it('should handle case sensitivity for public site visibility', async () => {
mockSiteTreeItem = new SiteTreeItem({
name: "Test Site",
Expand Down