fix: "Add to Twenty" button render fix (chrome-extension) (#5048)

fix - #5047
This commit is contained in:
Aditya Pimpalkar
2024-04-19 17:13:53 +01:00
committed by GitHub
parent d145684966
commit 14f97e2e80
13 changed files with 318 additions and 308 deletions

View File

@ -1,12 +1,17 @@
const createNewButton = (
text: string,
onClickHandler: () => void,
): HTMLDivElement => {
import { isDefined } from '~/utils/isDefined';
export const createDefaultButton = (
buttonId: string,
onClickHandler?: () => void,
buttonText = '',
) => {
const btn = document.getElementById(buttonId);
if (isDefined(btn)) return btn;
const div = document.createElement('div');
const img = document.createElement('img');
const span = document.createElement('span');
span.textContent = text;
span.textContent = buttonText;
img.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
img.height = 16;
@ -32,43 +37,38 @@ const createNewButton = (
Object.assign(div.style, divStyles);
// // Apply common styles to the button.
// Object.assign(buttonDiv.style, buttonDivStyles);
// Apply common styles to specifc states of a button.
div.addEventListener('mouseenter', () => {
const hoverStyles = {
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
backgroundColor: '#5e5e5e',
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
borderColor: '#5e5e5e',
};
Object.assign(div.style, hoverStyles);
});
// // Apply common styles to specifc states of a button.
// newButton.addEventListener('mouseenter', () => {
// const hoverStyles = {
// backgroundColor: '#5e5e5e',
// borderColor: '#5e5e5e',
// };
// Object.assign(newButton.style, hoverStyles);
// });
div.addEventListener('mouseleave', () => {
Object.assign(div.style, divStyles);
});
// newButton.addEventListener('mouseleave', () => {
// Object.assign(newButton.style, buttonStyles);
// });
// Handle the click event.
div.addEventListener('click', async () => {
const { apiKey } = await chrome.storage.local.get('apiKey');
div.addEventListener('click', async (e) => {
e.preventDefault();
const store = await chrome.storage.local.get();
// If an api key is not set, the options page opens up to allow the user to configure an api key.
if (!apiKey) {
if (!store.apiKey) {
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
return;
}
// Update content during the resolution of the request.
span.textContent = 'Saving...';
// Call the provided onClickHandler function to handle button click logic
onClickHandler();
onClickHandler?.();
});
div.id = buttonId;
div.appendChild(img);
div.appendChild(span);
return div;
};
export default createNewButton;

View File

@ -1,123 +1,117 @@
import createNewButton from '~/contentScript/createButton';
import { createDefaultButton } from '~/contentScript/createButton';
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
import extractDomain from '~/contentScript/utils/extractDomain';
import { createCompany, fetchCompany } from '~/db/company.db';
import { CompanyInput } from '~/db/types/company.types';
import { isDefined } from '~/utils/isDefined';
export const checkIfCompanyExists = async () => {
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
const companyURL = extractCompanyLinkedinLink(activeTab.url);
return await fetchCompany({
linkedinLink: {
url: { eq: companyURL },
label: { eq: companyURL },
},
});
};
export const addCompany = async () => {
// Extract company-specific data from the DOM
const companyNameElement = document.querySelector(
'.org-top-card-summary__title',
);
const domainNameElement = document.querySelector(
'.org-top-card-primary-actions__inner a',
);
const addressElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[1];
const employeesNumberElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[3];
// Get the text content or other necessary data from the DOM elements
const companyName = companyNameElement
? companyNameElement.getAttribute('title')
: '';
const domainName = extractDomain(
domainNameElement && domainNameElement.getAttribute('href'),
);
const address = addressElement
? addressElement.textContent?.trim().replace(/\s+/g, ' ')
: '';
const employees = employeesNumberElement
? Number(
employeesNumberElement.textContent
?.trim()
.replace(/\s+/g, ' ')
.split('-')[0],
)
: 0;
// Prepare company data to send to the backend
const companyInputData: CompanyInput = {
name: companyName ?? '',
domainName: domainName,
address: address ?? '',
employees: employees,
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
const companyURL = extractCompanyLinkedinLink(activeTab.url);
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
const company = await createCompany(companyInputData);
return company;
};
export const insertButtonForCompany = async () => {
const companyButtonDiv = createDefaultButton(
'twenty-company-btn',
async () => {
if (isDefined(companyButtonDiv)) {
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
companyBtnSpan.textContent = 'Saving...';
const company = await addCompany();
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
} else {
companyBtnSpan.textContent = 'Try again';
}
}
},
);
const insertButtonForCompany = async (): Promise<void> => {
// Select the element in which to create the button.
const parentDiv: HTMLDivElement | null = document.querySelector(
'.org-top-card-primary-actions__inner',
);
// Create the button with desired callback funciton to execute upon click.
if (parentDiv) {
// Extract company-specific data from the DOM
const companyNameElement = document.querySelector(
'.org-top-card-summary__title',
);
const domainNameElement = document.querySelector(
'.org-top-card-primary-actions__inner a',
);
const addressElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[1];
const employeesNumberElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[3];
// Get the text content or other necessary data from the DOM elements
const companyName = companyNameElement
? companyNameElement.getAttribute('title')
: '';
const domainName = extractDomain(
domainNameElement && domainNameElement.getAttribute('href'),
);
const address = addressElement
? addressElement.textContent?.trim().replace(/\s+/g, ' ')
: '';
const employees = employeesNumberElement
? Number(
employeesNumberElement.textContent
?.trim()
.replace(/\s+/g, ' ')
.split('-')[0],
)
: 0;
// Prepare company data to send to the backend
const companyInputData: CompanyInput = {
name: companyName ?? '',
domainName: domainName,
address: address ?? '',
employees: employees,
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
const { url: activeTabUrl } = await chrome.runtime.sendMessage({
action: 'getActiveTabUrl',
if (isDefined(parentDiv)) {
Object.assign(companyButtonDiv.style, {
marginLeft: '.8rem',
marginTop: '.4rem',
});
parentDiv.prepend(companyButtonDiv);
}
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
const companyURL = extractCompanyLinkedinLink(activeTabUrl);
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
const company = await checkIfCompanyExists();
const company = await fetchCompany({
linkedinLink: {
url: { eq: companyURL },
label: { eq: companyURL },
},
});
if (company) {
const savedCompany: HTMLDivElement = createNewButton(
'Saved',
async () => {},
);
// Include the button in the DOM.
parentDiv.prepend(savedCompany);
// Write button specific styles here - common ones can be found in createButton.ts.
const buttonSpecificStyles = {
alignSelf: 'end',
};
Object.assign(savedCompany.style, buttonSpecificStyles);
} else {
const newButtonCompany: HTMLDivElement = createNewButton(
'Add to Twenty',
async () => {
const response = await createCompany(companyInputData);
if (response) {
newButtonCompany.textContent = 'Saved';
newButtonCompany.setAttribute('disabled', 'true');
// Button specific styles once the button is unclickable after successfully sending data to server.
newButtonCompany.addEventListener('mouseenter', () => {
const hoverStyles = {
backgroundColor: 'black',
borderColor: 'black',
cursor: 'default',
};
Object.assign(newButtonCompany.style, hoverStyles);
});
} else {
newButtonCompany.textContent = 'Try Again';
}
},
);
// Include the button in the DOM.
parentDiv.prepend(newButtonCompany);
// Write button specific styles here - common ones can be found in createButton.ts.
const buttonSpecificStyles = {
alignSelf: 'end',
};
Object.assign(newButtonCompany.style, buttonSpecificStyles);
}
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
} else {
companyBtnSpan.textContent = 'Add to Twenty';
}
};
export default insertButtonForCompany;

View File

@ -1,127 +1,125 @@
import createNewButton from '~/contentScript/createButton';
import { createDefaultButton } from '~/contentScript/createButton';
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
import { createPerson, fetchPerson } from '~/db/person.db';
import { PersonInput } from '~/db/types/person.types';
import { isDefined } from '~/utils/isDefined';
const insertButtonForPerson = async (): Promise<void> => {
// Select the element in which to create the button.
const parentDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas',
export const checkIfPersonExists = async () => {
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
let activeTabUrl = '';
if (isDefined(activeTab.url.endsWith('/'))) {
activeTabUrl = activeTab.url.slice(0, -1);
}
const personNameElement = document.querySelector('.text-heading-xlarge');
const personName = personNameElement ? personNameElement.textContent : '';
const { firstName, lastName } = extractFirstAndLastName(String(personName));
const person = await fetchPerson({
name: {
firstName: { eq: firstName },
lastName: { eq: lastName },
},
linkedinLink: { url: { eq: activeTabUrl }, label: { eq: activeTabUrl } },
});
return person;
};
export const addPerson = async () => {
const personNameElement = document.querySelector('.text-heading-xlarge');
const separatorElement = document.querySelector(
'.pv-text-details__separator',
);
const personCityElement = separatorElement?.previousElementSibling;
const profilePictureElement = document.querySelector(
'.pv-top-card-profile-picture__image',
);
// Create the button with desired callback funciton to execute upon click.
if (parentDiv) {
// Extract person-specific data from the DOM.
const personNameElement = document.querySelector('.text-heading-xlarge');
const firstListItem = document.querySelector(
'div[data-view-name="profile-component-entity"]',
);
const secondDivElement = firstListItem?.querySelector('div:nth-child(2)');
const ariaHiddenSpan = secondDivElement?.querySelector(
'span[aria-hidden="true"]',
);
const separatorElement = document.querySelector(
'.pv-text-details__separator',
);
const personCityElement = separatorElement?.previousElementSibling;
// Get the text content or other necessary data from the DOM elements.
const personName = personNameElement ? personNameElement.textContent : '';
const personCity = personCityElement
? personCityElement.textContent?.trim().replace(/\s+/g, ' ').split(',')[0]
: '';
const profilePicture = profilePictureElement
? profilePictureElement?.getAttribute('src')
: '';
const jobTitle = ariaHiddenSpan ? ariaHiddenSpan.textContent?.trim() : '';
const profilePictureElement = document.querySelector(
'.pv-top-card-profile-picture__image',
const { firstName, lastName } = extractFirstAndLastName(String(personName));
// Prepare person data to send to the backend.
const personData: PersonInput = {
name: { firstName, lastName },
city: personCity ?? '',
avatarUrl: profilePicture ?? '',
jobTitle: jobTitle ?? '',
linkedinLink: { url: '', label: '' },
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
let activeTabUrl = '';
// Remove last slash from the URL for consistency when saving usernames.
if (isDefined(activeTab.url.endsWith('/'))) {
activeTabUrl = activeTab.url.slice(0, -1);
}
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const person = await createPerson(personData);
return person;
};
export const insertButtonForPerson = async () => {
const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
if (isDefined(personButtonDiv)) {
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
personBtnSpan.textContent = 'Saving...';
const person = await addPerson();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else {
personBtnSpan.textContent = 'Try again';
}
}
});
if (isDefined(personButtonDiv)) {
const parentDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas',
);
const firstListItem = document.querySelector(
'div[data-view-name="profile-component-entity"]',
);
const secondDivElement = firstListItem?.querySelector('div:nth-child(2)');
const ariaHiddenSpan = secondDivElement?.querySelector(
'span[aria-hidden="true"]',
);
// Get the text content or other necessary data from the DOM elements.
const personName = personNameElement ? personNameElement.textContent : '';
const personCity = personCityElement
? personCityElement.textContent?.trim().replace(/\s+/g, ' ').split(',')[0]
: '';
const profilePicture = profilePictureElement
? profilePictureElement?.getAttribute('src')
: '';
const jobTitle = ariaHiddenSpan ? ariaHiddenSpan.textContent?.trim() : '';
const { firstName, lastName } = extractFirstAndLastName(String(personName));
// Prepare person data to send to the backend.
const personData: PersonInput = {
name: { firstName, lastName },
city: personCity ?? '',
avatarUrl: profilePicture ?? '',
jobTitle: jobTitle ?? '',
linkedinLink: { url: '', label: '' },
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
let { url: activeTabUrl } = await chrome.runtime.sendMessage({
action: 'getActiveTabUrl',
});
// Remove last slash from the URL for consistency when saving usernames.
if (activeTabUrl.endsWith('/')) {
activeTabUrl = activeTabUrl.slice(0, -1);
if (isDefined(parentDiv)) {
Object.assign(personButtonDiv.style, {
marginRight: '.8rem',
});
parentDiv.prepend(personButtonDiv);
}
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const person = await fetchPerson({
name: {
firstName: { eq: firstName },
lastName: { eq: lastName },
},
linkedinLink: { url: { eq: activeTabUrl }, label: { eq: activeTabUrl } },
});
if (person) {
const savedPerson: HTMLDivElement = createNewButton(
'Saved',
async () => {},
);
// Include the button in the DOM.
parentDiv.prepend(savedPerson);
// Write button specific styles here - common ones can be found in createButton.ts.
const buttonSpecificStyles = {
marginRight: '0.5em',
};
Object.assign(savedPerson.style, buttonSpecificStyles);
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
const person = await checkIfPersonExists();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else {
const newButtonPerson: HTMLDivElement = createNewButton(
'Add to Twenty',
async () => {
const response = await createPerson(personData);
if (response) {
newButtonPerson.textContent = 'Saved';
newButtonPerson.setAttribute('disabled', 'true');
// Button specific styles once the button is unclickable after successfully sending data to server.
newButtonPerson.addEventListener('mouseenter', () => {
const hoverStyles = {
backgroundColor: 'black',
borderColor: 'black',
cursor: 'default',
};
Object.assign(newButtonPerson.style, hoverStyles);
});
} else {
newButtonPerson.textContent = 'Try Again';
}
},
);
// Include the button in the DOM.
parentDiv.prepend(newButtonPerson);
// Write button specific styles here - common ones can be found in createButton.ts.
const buttonSpecificStyles = {
marginRight: '0.5em',
};
Object.assign(newButtonPerson.style, buttonSpecificStyles);
personBtnSpan.textContent = 'Add to Twenty';
}
}
};
export default insertButtonForPerson;

View File

@ -1,10 +1,13 @@
import insertButtonForCompany from '~/contentScript/extractCompanyProfile';
import insertButtonForPerson from '~/contentScript/extractPersonProfile';
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
// Inject buttons into the DOM when SPA is reloaded on the resource url.
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
await insertButtonForCompany();
await insertButtonForPerson();
// await insertButtonForCompany();
(async () => {
await insertButtonForCompany();
await insertButtonForPerson();
})();
// The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/.
// However, there would never be another reload in a single page application unless triggered manually.

View File

@ -1,4 +1,7 @@
// Extract "https://www.linkedin.com/company/twenty/" from any of the following urls, which the user can visit while on the company page.
import { isDefined } from '~/utils/isDefined';
// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
const extractCompanyLinkedinLink = (activeTabUrl: string) => {
// Regular expression to match the company ID
@ -7,7 +10,7 @@ const extractCompanyLinkedinLink = (activeTabUrl: string) => {
// Extract the company ID using the regex
const match = activeTabUrl.match(regex);
if (match && match[1]) {
if (isDefined(match) && isDefined(match[1])) {
const companyID = match[1];
const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
return cleanCompanyURL;