Skip to content
Open
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
105 changes: 74 additions & 31 deletions src/commands/texei/sharingcalc/recalculate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
requiredOrgFlagWithDeprecations,
loglevel,
} from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Messages, SfError } from '@salesforce/core';
import * as puppeteer from 'puppeteer';

// Initialize Messages with the current plugin directory
Expand All @@ -28,6 +28,21 @@ export type SharingcalcRecalculateResult = {

const mapSharingLabel = new Map([['sharingRule', 'Sharing Rule']]);

const SELECTORS = {
sharingRule: '#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_recalc"].btn',
};

const WAIT_OPTIONS = {
navigation: {
waitUntil: ['domcontentloaded', 'networkidle2'],
timeout: 60000,
} as puppeteer.WaitForOptions,
selector: {
visible: true,
timeout: 5000,
},
};

export default class Recalculate extends SfCommand<SharingcalcRecalculateResult> {
public static readonly summary = messages.getMessage('summary');

Expand All @@ -50,54 +65,82 @@ export default class Recalculate extends SfCommand<SharingcalcRecalculateResult>
public async run(): Promise<SharingcalcRecalculateResult> {
const { flags } = await this.parse(Recalculate);

const result = await this.reclaculateSharing(flags);

// Process operation
const result = await this.recalculateSharing(flags);
return { message: result };
}

private async reclaculateSharing(flags) {
const instanceUrl = flags['target-org'].getConnection(flags['api-version']).instanceUrl;
private async recalculateSharing(flags): Promise<string> {
this.spinner.start(`Recalculating ${mapSharingLabel.get(flags.scope)} Calculations`, undefined, { stdout: true });

const SHARING_CALC_PATH = '/p/own/DeferSharingSetupPage';
let browser: puppeteer.Browser | null = null;

this.spinner.start(`Resuming ${mapSharingLabel.get(flags.scope)} Calculations`, undefined, { stdout: true });
this.debug('DEBUG Login to Org');
try {
// Initialize browser
browser = await this.initializeBrowser();

// Navigate to sharing page
const page = await this.navigateToSharingPage(browser, flags);

// Perform recalculate action
await this.performRecalculateAction(page, flags.scope);

this.spinner.stop('Done.');
return `Recalculated ${mapSharingLabel.get(flags.scope)}s`;
} catch (error) {
this.spinner.stop('Failed.');
throw new SfError(`Failed to recalculate sharing calculations: ${error.message}`);
} finally {
if (browser) {
this.debug('DEBUG Closing browser');
await browser.close();
}
}
}

private async initializeBrowser(): Promise<puppeteer.Browser> {
this.debug('DEBUG Initializing browser');

const browser = await puppeteer.launch({
return puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: !(process.env.BROWSER_DEBUG === 'true'),
});
}

private async navigateToSharingPage(browser: puppeteer.Browser, flags): Promise<puppeteer.Page> {
const SHARING_CALC_PATH = '/p/own/DeferSharingSetupPage';

const page = await browser.newPage();
await page.goto(
`${instanceUrl}/secur/frontdoor.jsp?sid=${flags['target-org'].getConnection(flags['api-version']).accessToken}`,
{ waitUntil: ['domcontentloaded', 'networkidle2'] }
);
const navigationPromise = page.waitForNavigation();

this.debug('DEBUG Opening Defer Sharing Calculations page');
// Login to Org via frontdoor
const connection = flags['target-org'].getConnection(flags['api-version']);
const instanceUrl = connection.instanceUrl;
const accessToken = connection.accessToken;

await page.goto(`${instanceUrl + SHARING_CALC_PATH}`);
await navigationPromise;
this.debug('DEBUG Login to Org');
const loginUrl = `${instanceUrl}/secur/frontdoor.jsp?sid=${accessToken}`;
await page.goto(loginUrl, WAIT_OPTIONS.navigation);

this.debug("DEBUG Clicking 'Recalculate' button");
// Navigate to Sharing Calculations page
this.debug('DEBUG Opening Defer Sharing Calculations page');
await page.goto(`${instanceUrl}${SHARING_CALC_PATH}`, WAIT_OPTIONS.navigation);

try {
await page.click(
'#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_recalc"].btn'
);
} catch (ex) {
// eslint-disable-next-line no-console
console.log('Unable to recalculate sharing.', ex.message);
}
return page;
}

await navigationPromise;
private async performRecalculateAction(page: puppeteer.Page, scope: string): Promise<void> {
this.debug("DEBUG Clicking 'Recalculate' button");

this.debug('DEBUG Closing browser');
// Get the appropriate selector for the scope
const selector = SELECTORS[scope] || SELECTORS.sharingRule;
this.debug(`DEBUG Using selector: ${selector}`);

await browser.close();
// Wait for element to be visible and clickable
await page.waitForSelector(selector, WAIT_OPTIONS.selector);

this.spinner.stop('Done.');
// Perform click and wait for navigation simultaneously
await Promise.all([page.waitForNavigation(WAIT_OPTIONS.navigation), page.click(selector)]);

return `Recalculated ${mapSharingLabel.get(flags.scope)}s`;
this.debug('DEBUG Recalculate action completed successfully');
}
}
145 changes: 101 additions & 44 deletions src/commands/texei/sharingcalc/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ const mapSharingLabel = new Map([
['groupMembership', 'Group Membership'],
]);

const SELECTORS = {
groupMembership:
'#gmSect > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="group_resume"].btn',
sharingRule: '#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_resume"].btn',
groupResumeDialog: 'div#group_resume_dialog_buttons > input[value=" Yes "]',
};

const WAIT_OPTIONS = {
navigation: {
waitUntil: ['domcontentloaded', 'networkidle2'],
timeout: 60000,
} as puppeteer.WaitForOptions,
selector: {
visible: true,
timeout: 5000,
},
};

export default class Resume extends SfCommand<SharingcalcResumeResult> {
public static readonly summary = messages.getMessage('summary');

Expand Down Expand Up @@ -69,70 +87,109 @@ export default class Resume extends SfCommand<SharingcalcResumeResult> {
}
}, flags.timeout);

// Process operation
const result = await this.resumeSharingCalc(flags);

// Clear timeout handler
// @ts-ignore: TODO: working code, but look at TS warning
clearTimeout(this.timeoutHandler);
this.timeoutHandler = null;

return { message: result };
try {
// Process operation
const result = await this.resumeSharingCalc(flags);
return { message: result };
} finally {
// Clear timeout handler
// @ts-ignore: TODO: working code, but look at TS warning
clearTimeout(this.timeoutHandler);
this.timeoutHandler = null;
}
}

private async resumeSharingCalc(flags) {
const instanceUrl = flags['target-org'].getConnection(flags['api-version']).instanceUrl;
private async resumeSharingCalc(flags): Promise<string> {
this.spinner.start(`Resuming ${mapSharingLabel.get(flags.scope)} Calculations`, undefined, { stdout: true });

const SHARING_CALC_PATH = '/p/own/DeferSharingSetupPage';
let browser: puppeteer.Browser | null = null;

this.spinner.start(`Resuming ${mapSharingLabel.get(flags.scope)} Calculations`, undefined, { stdout: true });
this.debug('DEBUG Login to Org');
try {
// Initialize browser
browser = await this.initializeBrowser();

// Navigate to sharing page
const page = await this.navigateToSharingPage(browser, flags);

// Perform resume action
await this.performResumeAction(page, flags.scope);

this.spinner.stop('Done.');
return `Resumed ${mapSharingLabel.get(flags.scope)} Calculations`;
} catch (error) {
this.spinner.stop('Failed.');
throw new SfError(`Failed to resume sharing calculations: ${error.message}`);
} finally {
if (browser) {
this.debug('DEBUG Closing browser');
await browser.close();
}
}
}

private async initializeBrowser(): Promise<puppeteer.Browser> {
this.debug('DEBUG Initializing browser');

const browser = await puppeteer.launch({
return puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: !(process.env.BROWSER_DEBUG === 'true'),
});
}

private async navigateToSharingPage(browser: puppeteer.Browser, flags): Promise<puppeteer.Page> {
const SHARING_CALC_PATH = '/p/own/DeferSharingSetupPage';

const page = await browser.newPage();
await page.goto(
`${instanceUrl}/secur/frontdoor.jsp?sid=${flags['target-org'].getConnection(flags['api-version']).accessToken}`,
{ waitUntil: ['domcontentloaded', 'networkidle2'] }
);
const navigationPromise = page.waitForNavigation();

// Login to Org via frontdoor
const connection = flags['target-org'].getConnection(flags['api-version']);
const instanceUrl = connection.instanceUrl;
const accessToken = connection.accessToken;

this.debug('DEBUG Login to Org');
const loginUrl = `${instanceUrl}/secur/frontdoor.jsp?sid=${accessToken}`;
await page.goto(loginUrl, WAIT_OPTIONS.navigation);

// Navigate to Sharing Calculations page
this.debug('DEBUG Opening Defer Sharing Calculations page');
await page.goto(`${instanceUrl}${SHARING_CALC_PATH}`, WAIT_OPTIONS.navigation);

await page.goto(`${instanceUrl + SHARING_CALC_PATH}`);
await navigationPromise;
return page;
}

private async performResumeAction(page: puppeteer.Page, scope: string): Promise<void> {
this.debug("DEBUG Clicking 'Resume' button");

try {
// Resume either Group Membership or Sharing Rules
if (flags.scope === 'groupMembership') {
await page.click(
'#gmSect > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="group_resume"].btn'
);

// click the yes button to recaulcate group memberships immediately
await page.click('div#group_resume_dialog_buttons > input[value=" Yes "]');
} else {
await page.click(
'#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_resume"].btn'
);
}
} catch (ex) {
// eslint-disable-next-line no-console
console.log('Unable to resume sharing.', ex.message);
// Get the appropriate selector for the scope
const selector = SELECTORS[scope] || SELECTORS.sharingRule;
this.debug(`DEBUG Using selector: ${selector}`);

// Wait for element to be visible and clickable
await page.waitForSelector(selector, WAIT_OPTIONS.selector);

if (scope === 'groupMembership') {
// For group membership, we need to handle the confirmation dialog
await this.handleGroupMembershipResume(page, selector);
} else {
// For sharing rules, simple click and wait for navigation
await Promise.all([page.waitForNavigation(WAIT_OPTIONS.navigation), page.click(selector)]);
}

await navigationPromise;
this.debug('DEBUG Resume action completed successfully');
}

private async handleGroupMembershipResume(page: puppeteer.Page, selector: string): Promise<void> {
this.debug('DEBUG Handling group membership resume with confirmation dialog');

this.debug('DEBUG Closing browser');
// Click the resume button
await page.click(selector);

await browser.close();
// Wait for and click the confirmation dialog "Yes" button
this.debug('DEBUG Waiting for confirmation dialog');
await page.waitForSelector(SELECTORS.groupResumeDialog, WAIT_OPTIONS.selector);

this.spinner.stop('Done.');
await Promise.all([page.waitForNavigation(WAIT_OPTIONS.navigation), page.click(SELECTORS.groupResumeDialog)]);

return `Resumed ${mapSharingLabel.get(flags.scope)} Calculations`;
this.debug('DEBUG Group membership resume confirmation completed');
}
}
Loading