Skip to content

Commit

Permalink
feat: add support for Omni- and Service-Channel Settings and provide …
Browse files Browse the repository at this point in the history
…access to presence status via Permission Sets (#658)

- Adds the ability to enable the status-based capacity model for Omni-Channel
- Adds the ability to configure the status-based capacity model for a specific Service Channel
- Adds the ability to provide access to a presence status via Permission Sets (#564)
  • Loading branch information
ClayChipps authored Jan 11, 2025
1 parent dbd9cc0 commit 3155232
Show file tree
Hide file tree
Showing 22 changed files with 702 additions and 5 deletions.
13 changes: 8 additions & 5 deletions config/project-scratch-def.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"orgName": "Browserforce",
"edition": "Developer",
"language": "en_US",
"features": [
"CPQ",
"DeferSharingCalc",
"HighVelocitySales"
]
"features": ["CPQ", "DeferSharingCalc", "HighVelocitySales", "ServiceCloud"],
"settings": {
"omniChannelSettings": {
"enableOmniChannel": true,
"enableOmniSkillsRouting": true,
"enableOmniSecondaryRoutingPriority": true
}
}
}
6 changes: 6 additions & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import { HighVelocitySalesSettings as highVelocitySalesSettings } from './high-v
import { HomePageLayouts as homePageLayouts } from './home-page-layouts/index.js';
import { LightningExperienceSettings as lightningExperienceSettings } from './lightning-experience-settings/index.js';
import { LinkedInSalesNavigatorSettings as linkedInSalesNavigatorSettings } from './linkedin-sales-navigator-settings/index.js';
import { OmniChannelSettings as omniChannelSettings } from './omni-channel-settings/index.js';
import { OpportunitySplits as opportunitySplits } from './opportunity-splits/index.js';
import { PermissionSets as permissionSets } from './permission-sets/index.js';
import { Picklists as picklists } from './picklists/index.js';
import { RecordTypes as recordTypes } from './record-types/index.js';
import { RelateContactToMultipleAccounts as relateContactToMultipleAccounts } from './relate-contact-to-multiple-accounts/index.js';
import { ReportsAndDashboards as reportsAndDashboards } from './reports-and-dashboards/index.js';
import { SalesforceCpqConfig as salesforceCpqConfig } from './salesforce-cpq-config/index.js';
import { SalesforceToSalesforce as salesforceToSalesforce } from './salesforce-to-salesforce/index.js';
import { Security as security } from './security/index.js';
import { ServiceChannels as serviceChannels } from './service-channels/index.js';
import { Slack as slack } from './slack/index.js';

export {
Expand All @@ -31,13 +34,16 @@ export {
homePageLayouts,
lightningExperienceSettings,
linkedInSalesNavigatorSettings,
omniChannelSettings,
opportunitySplits,
permissionSets,
picklists,
recordTypes,
relateContactToMultipleAccounts,
reportsAndDashboards,
salesforceCpqConfig,
salesforceToSalesforce,
security,
serviceChannels,
slack
};
29 changes: 29 additions & 0 deletions src/plugins/omni-channel-settings/index.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from 'assert';
import { OmniChannelSettings } from './index.js';

describe(OmniChannelSettings.name, function () {
this.timeout('10m');
let plugin: OmniChannelSettings;
before(() => {
plugin = new OmniChannelSettings(global.bf);
});

const configEnableStatusBasedCapacityModel = {
enableStatusBasedCapacityModel: true
};
const configDisableStatusBasedCapacityModel = {
enableStatusBasedCapacityModel: false
};

it('should enable status based capacity model', async () => {
await plugin.run(configEnableStatusBasedCapacityModel);
const res = await plugin.retrieve();
assert.deepStrictEqual(res, configEnableStatusBasedCapacityModel);
});

it('should disable status based capacity model', async () => {
await plugin.run(configDisableStatusBasedCapacityModel);
const res = await plugin.retrieve();
assert.deepStrictEqual(res, configDisableStatusBasedCapacityModel);
});
});
46 changes: 46 additions & 0 deletions src/plugins/omni-channel-settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { BrowserforcePlugin } from '../../plugin.js';

const PATHS = {
BASE: 'omnichannel/settings.apexp'
};

const SELECTORS = {
SAVE_BUTTON: 'input[id$=":save"]',
STATUS_CAPACITY_TOGGLE: 'input[id$=":toggleOmniStatusCapModelPref"]'
};

type Config = {
enableStatusBasedCapacityModel?: boolean;
};

export class OmniChannelSettings extends BrowserforcePlugin {
public async retrieve(definition?: Config): Promise<Config> {
// Open the omni-channel setup page
const page = await this.browserforce.openPage(PATHS.BASE);

// Retrieve the service channel config
await page.waitForSelector(SELECTORS.STATUS_CAPACITY_TOGGLE);
const enableStatusBasedCapacityModel = await page.$eval(SELECTORS.STATUS_CAPACITY_TOGGLE, el => (el.getAttribute('checked') === "checked" ? true : false));

return { enableStatusBasedCapacityModel };
}

public async apply(config: Config): Promise<void> {
// Open the omni-channel setup page
const page = await this.browserforce.openPage(PATHS.BASE);

// Click the checkbox
const capacityModel = await page.waitForSelector(SELECTORS.STATUS_CAPACITY_TOGGLE);
await capacityModel.click();

// Save the settings
const saveButton = await page.waitForSelector(SELECTORS.SAVE_BUTTON);
await saveButton.click();

// Wait for the page to refresh
await page.waitForNavigation()

// Close the page
await page.close();
}
}
13 changes: 13 additions & 0 deletions src/plugins/omni-channel-settings/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/amtrack/sfdx-browserforce-plugin/src/plugins/omni-channel-settings/schema.json",
"title": "Omni-Channel Settings",
"type": "object",
"properties": {
"enableStatusBasedCapacityModel": {
"title": "Enable Status-Based Capacity Model",
"description": "Route and track work based on changes to work status and ownership.",
"type": "boolean"
}
}
}
40 changes: 40 additions & 0 deletions src/plugins/permission-sets/index.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from 'assert';
import * as child from 'child_process';
import { fileURLToPath } from 'node:url';
import * as path from 'path';
import { PermissionSets } from './index.js';

const __dirname = fileURLToPath(new URL('.', import.meta.url));

describe(PermissionSets.name, function () {
this.timeout('10m');
let plugin: PermissionSets;
before(() => {
plugin = new PermissionSets(global.bf);
});

const configurePermissionSet = [
{
permissionSetName: "ServicePresenceTest",
servicePresenceStatuses: ["TestStatus", "TestStatus3"]
}
];

it('should create permission set and service presence status as a prerequisite', () => {
const sourceDeployCmd = child.spawnSync('sf', [
'project',
'deploy',
'start',
'-d',
path.join(__dirname, 'sfdx-source'),
'--json'
]);
assert.deepStrictEqual(sourceDeployCmd.status, 0, sourceDeployCmd.output.toString());
});

it('should configure permission set presence status', async () => {
await plugin.run(configurePermissionSet);
const res = await plugin.retrieve(configurePermissionSet);
assert.deepStrictEqual(res, configurePermissionSet);
});
});
32 changes: 32 additions & 0 deletions src/plugins/permission-sets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BrowserforcePlugin } from '../../plugin.js';
import { ServicePresenceStatus } from './service-presence-status/index.js';

type PermissionSet = {
permissionSetName: string;
servicePresenceStatuses: string[];
};

export class PermissionSets extends BrowserforcePlugin {
public async retrieve(definition?: PermissionSet[]): Promise<PermissionSet[]> {
const pluginServicePresenceStatus = new ServicePresenceStatus(this.browserforce);

const permissionSets: PermissionSet[] = [];

for await (const permissionSet of definition) {
permissionSets.push({
permissionSetName: permissionSet.permissionSetName,
servicePresenceStatuses: await pluginServicePresenceStatus.retrieve(permissionSet)
});
}

return permissionSets;
}

public async apply(plan: PermissionSet[]): Promise<void> {
const pluginServicePresenceStatus = new ServicePresenceStatus(this.browserforce);

for await (const permissionSet of plan) {
await pluginServicePresenceStatus.apply(permissionSet);
}
}
}
24 changes: 24 additions & 0 deletions src/plugins/permission-sets/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/amtrack/sfdx-browserforce-plugin/src/plugins/permission-sets/schema.json",
"title": "Permission Sets",
"type": "array",
"items": { "$ref": "#/definitions/permissionSet" },
"default": [],
"definitions": {
"permissionSet": {
"type": "object",
"properties": {
"permissionSetName": {
"title": "Permission Set",
"description": "The name of the Permission Set to modify",
"type": "string"
},
"servicePresenceStatuses": {
"$ref": "./service-presence-status/schema.json"
}
},
"required": ["permissionSetName"]
}
}
}
80 changes: 80 additions & 0 deletions src/plugins/permission-sets/service-presence-status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { BrowserforcePlugin } from '../../../plugin.js';

const SELECTORS = {
ADD_BUTTON: 'a[id$=":duelingListBox:backingList_add"]',
REMOVE_BUTTON: 'a[id$=":duelingListBox:backingList_remove"]',
SAVE_BUTTON: 'input[id$=":button_pc_save"]',
VALUES_AVAILABLE: 'select[id$=":duelingListBox:backingList_a"]:not([disabled="disabled"])',
VALUES_ENABLED: 'select[id$=":duelingListBox:backingList_s"]:not([disabled="disabled"])'
};

type PermissionSet = {
permissionSetName: string;
servicePresenceStatuses: string[];
};

export class ServicePresenceStatus extends BrowserforcePlugin {
public async retrieve(definition: PermissionSet): Promise<string[]> {
// Query for the permission set
const permissionSetName = definition.permissionSetName;
const permissionSet = await this.org.getConnection().singleRecordQuery(
`SELECT Id FROM PermissionSet WHERE Name='${permissionSetName}'`
);

// Open the permission set setup page
const page = await this.browserforce.openPage(`${permissionSet.Id}/e?s=ServicePresenceStatusAccess`);

const enabledServicePresenceStatuses = await page.$$eval(`${SELECTORS.VALUES_ENABLED} > option`, (options) => {
return options.map((option) => option.title ?? '');
});

return enabledServicePresenceStatuses;
}

public async apply(config: PermissionSet): Promise<void> {
// Query for the permission set
const permissionSetName = config.permissionSetName;
const permissionSet = await this.org.getConnection().singleRecordQuery(
`SELECT Id FROM PermissionSet WHERE Name='${permissionSetName}'`
);

// Open the permission set setup page
const page = await this.browserforce.openPage(`${permissionSet.Id}/e?s=ServicePresenceStatusAccess`);

if (config?.servicePresenceStatuses) {
await page.waitForSelector(`${SELECTORS.VALUES_AVAILABLE} > option`);

const availableElements = await page.$$(`${SELECTORS.VALUES_AVAILABLE} > option`);

for (const availableElement of availableElements) {
const optionTitle = (await availableElement.evaluate(node => node.getAttribute('title')))?.toString();

if (optionTitle && config.servicePresenceStatuses.includes(optionTitle)) {
await availableElement.click();
await page.click(SELECTORS.ADD_BUTTON);
}
}

await page.waitForSelector(`${SELECTORS.VALUES_ENABLED} > option`);
const enabledElements = await page.$$(`${SELECTORS.VALUES_ENABLED} > option`);

for (const enabledElement of enabledElements) {
const optionTitle = (await enabledElement.evaluate(node => node.getAttribute('title')))?.toString();

if (optionTitle && !config.servicePresenceStatuses.includes(optionTitle)) {
await enabledElement.click();
await page.click(SELECTORS.REMOVE_BUTTON);
}
}
}

// Save the settings and wait for page refresh
await Promise.all([
page.waitForNavigation(),
page.click(SELECTORS.SAVE_BUTTON)
]);

// Close the page
await page.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/amtrack/sfdx-browserforce-plugin/src/plugins/permission-sets/schema.json",
"title": "Service Presence Statuses",
"type": "array",
"default": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<hasActivationRequired>false</hasActivationRequired>
<label>ServicePresenceTest</label>
</PermissionSet>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<ServicePresenceStatus xmlns="http://soap.sforce.com/2006/04/metadata">
<label>TestStatus</label>
</ServicePresenceStatus>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<ServicePresenceStatus xmlns="http://soap.sforce.com/2006/04/metadata">
<label>TestStatus2</label>
</ServicePresenceStatus>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<ServicePresenceStatus xmlns="http://soap.sforce.com/2006/04/metadata">
<label>TestStatus3</label>
</ServicePresenceStatus>
9 changes: 9 additions & 0 deletions src/plugins/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,21 @@
"homePageLayouts": {
"$ref": "./home-page-layouts/schema.json"
},
"omniChannelSettings": {
"$ref": "./omni-channel-settings/schema.json"
},
"permissionSets": {
"$ref": "./permission-sets/schema.json"
},
"salesforceToSalesforce": {
"$ref": "./salesforce-to-salesforce/schema.json"
},
"security": {
"$ref": "./security/schema.json"
},
"serviceChannels": {
"$ref": "./service-channels/schema.json"
},
"slack": {
"$ref": "./slack/schema.json"
}
Expand Down
Loading

0 comments on commit 3155232

Please sign in to comment.