diff --git a/src/commands/texei/sharingcalc/recalculate.ts b/src/commands/texei/sharingcalc/recalculate.ts index 1f1fc0b..7849385 100644 --- a/src/commands/texei/sharingcalc/recalculate.ts +++ b/src/commands/texei/sharingcalc/recalculate.ts @@ -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 @@ -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 { public static readonly summary = messages.getMessage('summary'); @@ -50,54 +65,82 @@ export default class Recalculate extends SfCommand public async run(): Promise { 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 { + 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 { + 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 { + 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 { + 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'); } } diff --git a/src/commands/texei/sharingcalc/resume.ts b/src/commands/texei/sharingcalc/resume.ts index e9edf15..316a2aa 100644 --- a/src/commands/texei/sharingcalc/resume.ts +++ b/src/commands/texei/sharingcalc/resume.ts @@ -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 { public static readonly summary = messages.getMessage('summary'); @@ -69,70 +87,109 @@ export default class Resume extends SfCommand { } }, 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 { + 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 { + 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 { + 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 { 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 { + 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'); } } diff --git a/src/commands/texei/sharingcalc/suspend.ts b/src/commands/texei/sharingcalc/suspend.ts index 461fbf9..03e64e0 100644 --- a/src/commands/texei/sharingcalc/suspend.ts +++ b/src/commands/texei/sharingcalc/suspend.ts @@ -32,6 +32,23 @@ const mapSharingLabel = new Map([ ['groupMembership', 'Group Membership'], ]); +const SELECTORS = { + groupMembership: + '#gmSect > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="group_suspend"].btn', + sharingRule: '#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_suspend"].btn', +}; + +const WAIT_OPTIONS = { + navigation: { + waitUntil: ['domcontentloaded', 'networkidle2'], + timeout: 60000, + } as puppeteer.WaitForOptions, + selector: { + visible: true, + timeout: 5000, + }, +}; + export default class Suspend extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -70,71 +87,98 @@ export default class Suspend extends SfCommand { } }, flags.timeout); - // Process operation - const result = await this.suspendSharingCalc(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.suspendSharingCalc(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 suspendSharingCalc(flags) { - const instanceUrl = flags['target-org'].getConnection(flags['api-version']).instanceUrl; + private async suspendSharingCalc(flags): Promise { + this.spinner.start(`Suspending ${mapSharingLabel.get(flags.scope)} Calculations`, undefined, { stdout: true }); - const SHARING_CALC_PATH = '/p/own/DeferSharingSetupPage'; + let browser: puppeteer.Browser | null = null; - this.spinner.start(`Suspending ${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 suspend action + await this.performSuspendAction(page, flags.scope); + + this.spinner.stop('Done.'); + return `Suspended ${mapSharingLabel.get(flags.scope)} Calculations`; + } catch (error) { + this.spinner.stop('Failed.'); + throw new SfError(`Failed to suspend sharing calculations: ${error.message}`); + } finally { + if (browser) { + this.debug('DEBUG Closing browser'); + await browser.close(); + } + } + } - const browser = await puppeteer.launch({ + private async initializeBrowser(): Promise { + this.debug('DEBUG Initializing browser'); + + return puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: !(process.env.BROWSER_DEBUG === 'true'), }); + } + + private async navigateToSharingPage(browser: puppeteer.Browser, flags): Promise { + 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 performSuspendAction(page: puppeteer.Page, scope: string): Promise { this.debug("DEBUG Clicking 'Suspend' button"); - try { - // Suspend either Group Membership or Sharing Rules - if (flags.scope === 'groupMembership') { - page.on('dialog', async (dialog) => { - await dialog.accept(); - }); - - await page.click( - '#gmSect > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="group_suspend"].btn' - ); - } else { - await page.click( - '#ep > .pbBody > .pbSubsection > .detailList > tbody > .detailRow > td > input[name="rule_suspend"].btn' - ); - } - } catch (ex) { - // eslint-disable-next-line no-console - console.log('Unable to suspend sharing.', ex.message); + // Setup dialog handler for group membership confirmations + if (scope === 'groupMembership') { + page.on('dialog', async (dialog) => { + this.debug('DEBUG Accepting dialog confirmation'); + await dialog.accept(); + }); } - await navigationPromise; - - 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 `Suspended ${mapSharingLabel.get(flags.scope)} Calculations`; + this.debug('DEBUG Suspend action completed successfully'); } }