Skip to content

Commit

Permalink
Merge pull request #7 from hudec117/dev
Browse files Browse the repository at this point in the history
0.2.0
  • Loading branch information
hudec117 authored Oct 19, 2023
2 parents 1d6fe17 + 92d14de commit af28b9e
Show file tree
Hide file tree
Showing 37 changed files with 2,574 additions and 900 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Browser extension providing various QoL improvements in Salesforce, with emphasis on familiar design and excellent UX.

## Enhanced Public Group Membership Editing
## Enhanced Public Group/Queue Memberships Editing

<img src="https://github.com/hudec117/sf-niknax/assets/10981949/183413a9-fe52-4682-85dc-1bf77cd5c0f0" width="450">
<img src="https://github.com/hudec117/sf-niknax/assets/10981949/9e527d65-fc05-4799-b404-c324ac59ba56" width="450">
Expand Down
904 changes: 438 additions & 466 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 16 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sf-niknax",
"version": "0.1.0",
"version": "0.3.0",
"private": true,
"license": "AGPL-3.0-only",
"scripts": {
Expand All @@ -12,22 +12,24 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@salesforce-ux/design-system": "^2.21.1",
"vue": "^3.2.47"
"@salesforce-ux/design-system": "^2.22.0",
"vue": "^3.3.4",
"vue-debounce": "^4.0.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.1",
"@tsconfig/node18": "^2.0.1",
"@types/chrome": "^0.0.235",
"@rushstack/eslint-patch": "^1.5.1",
"@tsconfig/node18": "^18.2.2",
"@types/chrome": "^0.0.246",
"@types/node": "^18.16.18",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.3.2",
"eslint": "^8.42.0",
"eslint-plugin-vue": "^9.14.1",
"@babel/types": "^7.23.0",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.50.0",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all": "^4.1.5",
"typescript": "~5.0.4",
"vite": "^4.3.9",
"vue-tsc": "^1.6.5"
"typescript": "~5.2.2",
"vite": "^4.4.11",
"vue-tsc": "^1.8.15"
}
}
36 changes: 26 additions & 10 deletions public/background.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
// Note: async/await with the Chrome API seems to break the code.

const SF_NIKNAX_PAGE = 'sf-niknax.html';

const PAGE_DIMENSIONS = {
'edit-public-group-memberships': { width: 673, height: 537 },
'edit-queue-memberships': { width: 673, height: 537 },
'clone-user': { width: 620, height: 694 },
'bulk-freeze-users': { width: 620, height: 630 },
'quick-create-user': { width: 620, height: 630 },
};

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.operation === 'open-sf-niknax') {
// Get server host
const serverUrl = new URL(sender.url);
const serverHost = serverUrl.hostname;

// Get server host
// Only the session ID against the ".my.salesforce.com" domain is valid for API requests.
const serverHost = serverUrl.hostname.replace('.lightning.force.com', '.my.salesforce.com');

// Get user record ID from URL
const path = serverUrl.pathname.substring(1);
const userId = /[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}/.exec(path);
if (userId === null) {
// TODO: handle
}
const userId = /[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}/.exec(path) ?? undefined;

let queryOptions = { active: true, lastFocusedWindow: true };
chrome.tabs.query(queryOptions, ([userTab]) => {
// Construct page URL with server host
let niknaxUrl = chrome.runtime.getURL(SF_NIKNAX_PAGE);
niknaxUrl += `?host=${serverHost}&user=${userId}&tab=${userTab.id}`;
niknaxUrl += `?host=${serverHost}&tab=${userTab.id}&page=${request.page}`;

if (userId) {
niknaxUrl += `&user=${userId}`;
}

// Note: width/height here must always be larger than the resizeTo dimensions
// in each popup. Otherwise the scrollbars introduced by a small width/height
// will affect the resizeTo.
chrome.windows.create({
url: niknaxUrl,
type: 'popup',
width: 595,
height: 505
width: PAGE_DIMENSIONS[request.page].width,
height: PAGE_DIMENSIONS[request.page].height
});
});
} else if (request.operation == 'get-session-id') {
const serverUrl = `https://${request.host}`;

chrome.cookies.get({ name: 'sid', url: serverUrl }, function (cookie) {
chrome.cookies.get({ name: 'sid', url: serverUrl }, (cookie) => {
const sessionId = cookie ? cookie.value : '';

sendResponse({ id: sessionId });
Expand Down
124 changes: 124 additions & 0 deletions public/classic-content-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
window.onload = function () {
const isInPopupIFrame = document.location.pathname.includes('emptyHtmlDoc.html');
if (isInPopupIFrame) {
return;
}

const pageTypeElements = document.getElementsByClassName('pageType');
if (pageTypeElements.length === 0) {
return;
}

const isOnUserDetailPage = pageTypeElements[0].innerText === 'User';
const isOnUserListPage = pageTypeElements[0].innerText === 'All Users';
if (isOnUserDetailPage) {
injectEditPublicGroupMembershipsButton();
injectEditQueueMembershipsButton();
// injectCloneUserButton();
} else if (isOnUserListPage) {
// injectFreezeUsersButton();
}

function injectEditPublicGroupMembershipsButton() {
const editMembershipButton = document.createElement('input');
editMembershipButton.id = 'sf-niknax-edit-public-group-memberships-button';
editMembershipButton.value = 'Edit Memberships';
editMembershipButton.title = `Salesforce Niknax: ${editMembershipButton.value}`;
editMembershipButton.className = 'btn';
editMembershipButton.type = 'button';
editMembershipButton.style = 'margin-left: 5px; border: 1px solid #2574a9;';

// Setup event listeners, we also need to handle the pop-up panel for the links at the top of the page.
const rlPanelFrame = document.getElementById('RLPanelFrame');
rlPanelFrame.contentDocument.addEventListener('click', onClick);
document.addEventListener('click', onClick);

function onClick(event) {
if (event.target.id === editMembershipButton.id) {
chrome.runtime.sendMessage({ operation: 'open-sf-niknax', page: 'edit-public-group-memberships' });
}
}

// Find and inject
const publicGroupMembershipSection = getElementByStaticID('_RelatedPublicGroupMemberList');
if (!publicGroupMembershipSection) {
console.error('sf-niknax: failed to find *_RelatedPublicGroupMemberList element.');
return;
}

const buttonRow = publicGroupMembershipSection.querySelector('div.listRelatedObject.groupBlock > div > div.pbHeader > table > tbody > tr > td.pbButton');
buttonRow.appendChild(editMembershipButton);
}

function injectEditQueueMembershipsButton() {
const editMembershipButton = document.createElement('input');
editMembershipButton.id = 'sf-niknax-edit-queue-memberships-button';
editMembershipButton.value = 'Edit Memberships';
editMembershipButton.title = `Salesforce Niknax: ${editMembershipButton.value}`;
editMembershipButton.className = 'btn';
editMembershipButton.type = 'button';
editMembershipButton.style = 'margin-left: 5px; border: 1px solid #2574a9;';

const rlPanelFrame = document.getElementById('RLPanelFrame');
rlPanelFrame.contentDocument.addEventListener('click', onClick);
document.addEventListener('click', onClick);

function onClick(event) {
if (event.target.id === editMembershipButton.id) {
chrome.runtime.sendMessage({ operation: 'open-sf-niknax', page: 'edit-queue-memberships' });
}
}

// Find and inject
const queueMembershipSection = getElementByStaticID('_RelatedQueueMemberList');
if (!queueMembershipSection) {
console.error('sf-niknax: failed to find *_RelatedQueueMemberList element.');
return;
}

const buttonRow = queueMembershipSection.querySelector('div.listRelatedObject.setupBlock > div > div.pbHeader > table > tbody > tr > td.pbButton');
buttonRow.appendChild(editMembershipButton);
}

// function injectCloneUserButton() {
// const cloneUserButton = document.createElement('input');
// cloneUserButton.value = 'Clone';
// cloneUserButton.title = `Salesforce Niknax: ${cloneUserButton.value}`;
// cloneUserButton.className = 'btn';
// cloneUserButton.type = 'button';
// cloneUserButton.style = 'margin-left: 5px; border: 1px solid #2574a9;';
// cloneUserButton.addEventListener('click', () => {
// chrome.runtime.sendMessage({ operation: 'open-sf-niknax', page: 'clone-user' });
// });

// const topButtonRow = document.getElementById('topButtonRow');
// topButtonRow.appendChild(cloneUserButton);
// }

// function injectFreezeUsersButton() {
// const freezeUsersButton = document.createElement('input');
// freezeUsersButton.value = 'Bulk Freeze/Unfreeze';
// freezeUsersButton.title = `Salesforce Niknax: ${freezeUsersButton.value}`;
// freezeUsersButton.className = 'btn';
// freezeUsersButton.type = 'button';
// freezeUsersButton.style = 'margin-left: 5px; border: 1px solid #2574a9;';
// freezeUsersButton.addEventListener('click', () => {
// chrome.runtime.sendMessage({ operation: 'open-sf-niknax', page: 'bulk-freeze-users' });
// });

// const topButtonRow = document.querySelector('td.pbBottomButtons');
// topButtonRow.appendChild(freezeUsersButton);
// }

function getElementByStaticID(staticString) {
const divElements = document.querySelectorAll('div');

for (const element of divElements) {
if (element.id && element.id.includes(staticString)) {
return element;
}
}

return null;
}
}
41 changes: 0 additions & 41 deletions public/content-script.js

This file was deleted.

60 changes: 60 additions & 0 deletions public/lightning-content-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
window.onload = function () {
const QUICK_CREATE_USER_GLOBAL_ACTION_HTML = `
<div class="oneHelpAndTrainingExperience">
<button id="sf-niknax-quick-create-user-button" type="button" class="slds-button slds-button slds-button_icon slds-button_icon slds-button_icon-container slds-button_icon-small slds-global-actions__item-action forceHeaderButton" title="Salesforce Niknax: Quick Create User">
<lightning-icon icon-name="utility:adduser" class="slds-icon-utility-adduser slds-global-header__icon slds-button__icon slds-icon_container forceIcon">
<span part="boundary">
<lightning-primitive-icon>
<svg class="slds-icon slds-icon_xx-small" focusable="false" viewBox="0 0 52 52" part="icon">
<g>
<path d="M21.9,37c0-2.7,0.9-5.8,2.3-8.2c1.7-3,3.6-4.2,5.1-6.4c2.5-3.7,3-9,1.4-13c-1.6-4.1-5.4-6.5-9.8-6.4
s-8,2.8-9.4,6.9c-1.6,4.5-0.9,9.9,2.7,13.3c1.5,1.4,2.9,3.6,2.1,5.7c-0.7,2-3.1,2.9-4.8,3.7c-3.9,1.7-8.6,4.1-9.4,8.7
C1.3,45.1,3.9,49,8,49h17c0.8,0,1.3-1,0.8-1.6C23.3,44.5,21.9,40.8,21.9,37z"/>
<path d="M37.9,25c-6.6,0-12,5.4-12,12s5.4,12,12,12s12-5.4,12-12S44.5,25,37.9,25z M44,38c0,0.6-0.5,1-1.1,1H40v3
c0,0.6-0.5,1-1.1,1h-2c-0.6,0-0.9-0.4-0.9-1v-3h-3.1c-0.6,0-0.9-0.4-0.9-1v-2c0-0.6,0.3-1,0.9-1H36v-3c0-0.6,0.3-1,0.9-1h2
c0.6,0,1.1,0.4,1.1,1v3h2.9c0.6,0,1.1,0.4,1.1,1V38z"/>
</g>
</svg>
</lightning-primitive-icon>
</span>
</lightning-icon>
</button>
</div>
`;

// The inject is unreliable due to LX's long loading and so different pages (e.g. normal vs setup)
// would load differently requiring an interval timer to wait to find the right element to inject next to.
// We wait for up to a minute to allow for super slow loading times.
const MAX_TRIES = 240;
const INTERVAL = 250;
let tries = 0;
let intervalTimer;

intervalTimer = setInterval(() => {
const helpListItemDiv = document.querySelector('li.slds-global-actions__item div.oneHelpAndTrainingExperience');
if (helpListItemDiv) {
clearInterval(intervalTimer);
injectQuickCreateUserButton(helpListItemDiv);
} else {
tries++;
if (tries >= MAX_TRIES) {
clearInterval(intervalTimer);
console.warn('sf-niknax: failed to inject quick create user button.');
}
}
}, INTERVAL);

function injectQuickCreateUserButton(helpListItemDiv) {
const quickCreateUserListItem = document.createElement('li');
quickCreateUserListItem.className = 'slds-global-actions__item slds-dropdown-trigger slds-dropdown-trigger--click';
quickCreateUserListItem.innerHTML = QUICK_CREATE_USER_GLOBAL_ACTION_HTML;

// Go up to the "li" and insert ours after it.
helpListItemDiv.parentNode.after(quickCreateUserListItem);

const quickCreateUserButton = document.getElementById('sf-niknax-quick-create-user-button');
quickCreateUserButton.addEventListener('click', function() {
chrome.runtime.sendMessage({ operation: 'open-sf-niknax', page: 'quick-create-user' });
});
}
}
9 changes: 7 additions & 2 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Salesforce Niknax",
"description": "Various QoL and productivity improvements for Salesforce.",
"version": "0.1.0",
"version": "0.3.0",
"author": "Aurel Hudec",
"homepage_url": "https://github.com/hudec117/sf-niknax",
"incognito": "split",
Expand All @@ -21,7 +21,12 @@
{
"matches": [ "https://*.my.salesforce.com/*" ],
"all_frames": true,
"js": [ "content-script.js" ]
"js": [ "classic-content-script.js" ]
},
{
"matches": [ "https://*.lightning.force.com/*" ],
"all_frames": true,
"js": [ "lightning-content-script.js" ]
}
]
}
Loading

0 comments on commit af28b9e

Please sign in to comment.