diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts index 271224ee0..0140c3b6d 100644 --- a/packages/twenty-chrome-extension/src/background/index.ts +++ b/packages/twenty-chrome-extension/src/background/index.ts @@ -1,4 +1,5 @@ import { openOptionsPage } from '~/background/utils/openOptionsPage'; +import { isDefined } from '~/utils/isDefined'; // Open options page programmatically in a new tab. chrome.runtime.onInstalled.addListener((details) => { @@ -16,11 +17,10 @@ chrome.action.onClicked.addListener((tab) => { // The cases themselves are labelled such that their operations are reflected by their names. chrome.runtime.onMessage.addListener((message, _, sendResponse) => { switch (message.action) { - case 'getActiveTabUrl': // e.g. "https://linkedin.com/company/twenty/" + case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/" chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs && tabs[0]) { - const activeTabUrl: string | undefined = tabs[0].url; - sendResponse({ url: activeTabUrl }); + if (isDefined(tabs) && isDefined(tabs[0])) { + sendResponse({ tab: tabs[0] }); } }); break; @@ -34,27 +34,14 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { return true; }); -// Keep track of the tabs in which the "Add to Twenty" button has already been injected. -// Could be that the content script is executed at "https://linkedin.com/feed/", but is needed at "https://linkedin.com/in/mabdullahabaid/". -// However, since Linkedin is a SPA, the script would not be re-executed when you navigate to "https://linkedin.com/in/mabdullahabaid/" from a user action. -// Therefore, this tracks if the user is on desired route and then re-executes the content script to create the "Add to Twenty" button. -// We use a "Set" to keep track of tab ids because it could be that the "Add to Twenty" button was created at "https://linkedin/com/company/twenty". -// However, when we change to about on the company page, the url becomes "https://www.linkedin.com/company/twenty/about/" and the button is created again. -// This creates a duplicate button, which we want to avoid. So, we instruct the extension to only create the button once for any of the following urls. -// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/". -const injectedTabs: Set = new Set(); - chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { const isDesiredRoute = tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) || tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/); if (changeInfo.status === 'complete' && tab.active) { - if (isDesiredRoute && !injectedTabs.has(tabId)) { + if (isDefined(isDesiredRoute)) { chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' }); - injectedTabs.add(tabId); - } else if (!isDesiredRoute) { - injectedTabs.delete(tabId); // Clear entry if navigated away from LinkedIn company page. } } }); diff --git a/packages/twenty-chrome-extension/src/contentScript/createButton.ts b/packages/twenty-chrome-extension/src/contentScript/createButton.ts index 1fa9e9a3f..a566d1465 100644 --- a/packages/twenty-chrome-extension/src/contentScript/createButton.ts +++ b/packages/twenty-chrome-extension/src/contentScript/createButton.ts @@ -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; diff --git a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts index 5096438a0..7b59566c4 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts @@ -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 => { - // 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; diff --git a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts index 2d5076dfa..0fb36d0b1 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts @@ -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 => { - // 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; diff --git a/packages/twenty-chrome-extension/src/contentScript/index.ts b/packages/twenty-chrome-extension/src/contentScript/index.ts index 2cf8d378e..4df810e5c 100644 --- a/packages/twenty-chrome-extension/src/contentScript/index.ts +++ b/packages/twenty-chrome-extension/src/contentScript/index.ts @@ -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. diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts b/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts index 28e4f2d3f..31424e4cf 100644 --- a/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts +++ b/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts @@ -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; diff --git a/packages/twenty-chrome-extension/src/db/company.db.ts b/packages/twenty-chrome-extension/src/db/company.db.ts index f0936e1b4..7c3c78891 100644 --- a/packages/twenty-chrome-extension/src/db/company.db.ts +++ b/packages/twenty-chrome-extension/src/db/company.db.ts @@ -6,33 +6,42 @@ import { import { Company, CompanyFilterInput } from '~/generated/graphql'; import { CREATE_COMPANY } from '~/graphql/company/mutations'; import { FIND_COMPANY } from '~/graphql/company/queries'; +import { isDefined } from '~/utils/isDefined'; import { callMutation, callQuery } from '../utils/requestDb'; export const fetchCompany = async ( companyfilerInput: CompanyFilterInput, ): Promise => { - const data = await callQuery(FIND_COMPANY, { - filter: { - ...companyfilerInput, - }, - }); - if (data?.companies.edges) { - return data?.companies.edges.length > 0 - ? data?.companies.edges[0].node - : null; + try { + const data = await callQuery(FIND_COMPANY, { + filter: { + ...companyfilerInput, + }, + }); + if (isDefined(data?.companies.edges)) { + return data?.companies.edges.length > 0 + ? data?.companies.edges[0].node + : null; + } + return null; + } catch (error) { + return null; } - return null; }; export const createCompany = async ( company: CompanyInput, ): Promise => { - const data = await callMutation(CREATE_COMPANY, { - input: company, - }); - if (data) { - return data.createCompany.id; + try { + const data = await callMutation(CREATE_COMPANY, { + input: company, + }); + if (isDefined(data)) { + return data.createCompany.id; + } + return null; + } catch (error) { + return null; } - return null; }; diff --git a/packages/twenty-chrome-extension/src/db/person.db.ts b/packages/twenty-chrome-extension/src/db/person.db.ts index 29c782b38..a7df64710 100644 --- a/packages/twenty-chrome-extension/src/db/person.db.ts +++ b/packages/twenty-chrome-extension/src/db/person.db.ts @@ -6,31 +6,40 @@ import { import { Person, PersonFilterInput } from '~/generated/graphql'; import { CREATE_PERSON } from '~/graphql/person/mutations'; import { FIND_PERSON } from '~/graphql/person/queries'; +import { isDefined } from '~/utils/isDefined'; import { callMutation, callQuery } from '../utils/requestDb'; export const fetchPerson = async ( personFilterData: PersonFilterInput, ): Promise => { - const data = await callQuery(FIND_PERSON, { - filter: { - ...personFilterData, - }, - }); - if (data?.people.edges) { - return data?.people.edges.length > 0 ? data?.people.edges[0].node : null; + try { + const data = await callQuery(FIND_PERSON, { + filter: { + ...personFilterData, + }, + }); + if (isDefined(data?.people.edges)) { + return data?.people.edges.length > 0 ? data?.people.edges[0].node : null; + } + return null; + } catch (error) { + return null; } - return null; }; export const createPerson = async ( person: PersonInput, ): Promise => { - const data = await callMutation(CREATE_PERSON, { - input: person, - }); - if (data?.createPerson) { - return data.createPerson.id; + try { + const data = await callMutation(CREATE_PERSON, { + input: person, + }); + if (isDefined(data?.createPerson)) { + return data.createPerson.id; + } + return null; + } catch (error) { + return null; } - return null; }; diff --git a/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx b/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx index 524dc0c1f..1d198756e 100644 --- a/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx +++ b/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx @@ -5,6 +5,7 @@ import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Button } from '@/ui/input/button/Button'; import { TextInput } from '@/ui/input/components/TextInput'; import { Toggle } from '@/ui/input/components/Toggle'; +import { isDefined } from '~/utils/isDefined'; const StyledContainer = styled.div<{ isToggleOn: boolean }>` width: 400px; @@ -71,11 +72,11 @@ export const ApiKeyForm = () => { const getState = async () => { const localStorage = await chrome.storage.local.get(); - if (localStorage.apiKey) { + if (isDefined(localStorage.apiKey)) { setApiKey(localStorage.apiKey); } - if (localStorage.serverBaseUrl) { + if (isDefined(localStorage.serverBaseUrl)) { setShowSection(true); setRoute(localStorage.serverBaseUrl); } diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx index 61345232e..b390a94b9 100644 --- a/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; +import { isDefined } from '~/utils/isDefined'; + export type ToggleSize = 'small' | 'medium'; type ContainerProps = { @@ -54,7 +56,7 @@ export const Toggle = ({ const handleChange = () => { setIsOn(!isOn); - if (onChange) { + if (isDefined(onChange)) { onChange(!isOn); } }; diff --git a/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts b/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts index 0157fe0a7..c0396653f 100644 --- a/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts +++ b/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts @@ -14,7 +14,7 @@ const handleQueryParams = (inputData: { [x: string]: unknown }): string => { result = result.concat(`${key}: ${quote}${inputData[key]}${quote}, `); } }); - if (result.length) result = result.slice(0, -2); // Remove the last ', ' + if (result.length > 0) result = result.slice(0, -2); // Remove the last ', ' return result; }; diff --git a/packages/twenty-chrome-extension/src/utils/isDefined.ts b/packages/twenty-chrome-extension/src/utils/isDefined.ts new file mode 100644 index 000000000..81eb67203 --- /dev/null +++ b/packages/twenty-chrome-extension/src/utils/isDefined.ts @@ -0,0 +1,4 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +export const isDefined = (value: T | null | undefined): value is T => + !isUndefined(value) && !isNull(value); diff --git a/packages/twenty-chrome-extension/tsconfig.app.json b/packages/twenty-chrome-extension/tsconfig.app.json index 2f311488e..4b6b7d6c5 100644 --- a/packages/twenty-chrome-extension/tsconfig.app.json +++ b/packages/twenty-chrome-extension/tsconfig.app.json @@ -8,7 +8,7 @@ "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", - "jest.config.ts", + "jest.config.ts" ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }