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,4 +1,5 @@
import { openOptionsPage } from '~/background/utils/openOptionsPage'; import { openOptionsPage } from '~/background/utils/openOptionsPage';
import { isDefined } from '~/utils/isDefined';
// Open options page programmatically in a new tab. // Open options page programmatically in a new tab.
chrome.runtime.onInstalled.addListener((details) => { 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. // The cases themselves are labelled such that their operations are reflected by their names.
chrome.runtime.onMessage.addListener((message, _, sendResponse) => { chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
switch (message.action) { 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) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs && tabs[0]) { if (isDefined(tabs) && isDefined(tabs[0])) {
const activeTabUrl: string | undefined = tabs[0].url; sendResponse({ tab: tabs[0] });
sendResponse({ url: activeTabUrl });
} }
}); });
break; break;
@ -34,27 +34,14 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
return true; 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<number> = new Set();
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
const isDesiredRoute = const isDesiredRoute =
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) || tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/); tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
if (changeInfo.status === 'complete' && tab.active) { if (changeInfo.status === 'complete' && tab.active) {
if (isDesiredRoute && !injectedTabs.has(tabId)) { if (isDefined(isDesiredRoute)) {
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' }); chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
injectedTabs.add(tabId);
} else if (!isDesiredRoute) {
injectedTabs.delete(tabId); // Clear entry if navigated away from LinkedIn company page.
} }
} }
}); });

View File

@ -1,12 +1,17 @@
const createNewButton = ( import { isDefined } from '~/utils/isDefined';
text: string,
onClickHandler: () => void, export const createDefaultButton = (
): HTMLDivElement => { buttonId: string,
onClickHandler?: () => void,
buttonText = '',
) => {
const btn = document.getElementById(buttonId);
if (isDefined(btn)) return btn;
const div = document.createElement('div'); const div = document.createElement('div');
const img = document.createElement('img'); const img = document.createElement('img');
const span = document.createElement('span'); const span = document.createElement('span');
span.textContent = text; span.textContent = buttonText;
img.src = 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='; '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; img.height = 16;
@ -32,43 +37,38 @@ const createNewButton = (
Object.assign(div.style, divStyles); Object.assign(div.style, divStyles);
// // Apply common styles to the button. // Apply common styles to specifc states of a button.
// Object.assign(buttonDiv.style, buttonDivStyles); 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. div.addEventListener('mouseleave', () => {
// newButton.addEventListener('mouseenter', () => { Object.assign(div.style, divStyles);
// const hoverStyles = { });
// backgroundColor: '#5e5e5e',
// borderColor: '#5e5e5e',
// };
// Object.assign(newButton.style, hoverStyles);
// });
// newButton.addEventListener('mouseleave', () => { div.addEventListener('click', async (e) => {
// Object.assign(newButton.style, buttonStyles); e.preventDefault();
// }); const store = await chrome.storage.local.get();
// Handle the click event.
div.addEventListener('click', async () => {
const { apiKey } = await chrome.storage.local.get('apiKey');
// If an api key is not set, the options page opens up to allow the user to configure an api key. // 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' }); chrome.runtime.sendMessage({ action: 'openOptionsPage' });
return; return;
} }
// Update content during the resolution of the request. onClickHandler?.();
span.textContent = 'Saving...';
// Call the provided onClickHandler function to handle button click logic
onClickHandler();
}); });
div.id = buttonId;
div.appendChild(img); div.appendChild(img);
div.appendChild(span); div.appendChild(span);
return div; return div;
}; };
export default createNewButton;

View File

@ -1,17 +1,26 @@
import createNewButton from '~/contentScript/createButton'; import { createDefaultButton } from '~/contentScript/createButton';
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink'; import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
import extractDomain from '~/contentScript/utils/extractDomain'; import extractDomain from '~/contentScript/utils/extractDomain';
import { createCompany, fetchCompany } from '~/db/company.db'; import { createCompany, fetchCompany } from '~/db/company.db';
import { CompanyInput } from '~/db/types/company.types'; import { CompanyInput } from '~/db/types/company.types';
import { isDefined } from '~/utils/isDefined';
const insertButtonForCompany = async (): Promise<void> => { export const checkIfCompanyExists = async () => {
// Select the element in which to create the button. const { tab: activeTab } = await chrome.runtime.sendMessage({
const parentDiv: HTMLDivElement | null = document.querySelector( action: 'getActiveTab',
'.org-top-card-primary-actions__inner', });
);
// Create the button with desired callback funciton to execute upon click. const companyURL = extractCompanyLinkedinLink(activeTab.url);
if (parentDiv) {
return await fetchCompany({
linkedinLink: {
url: { eq: companyURL },
label: { eq: companyURL },
},
});
};
export const addCompany = async () => {
// Extract company-specific data from the DOM // Extract company-specific data from the DOM
const companyNameElement = document.querySelector( const companyNameElement = document.querySelector(
'.org-top-card-summary__title', '.org-top-card-summary__title',
@ -54,70 +63,55 @@ const insertButtonForCompany = async (): Promise<void> => {
}; };
// Extract active tab url using chrome API - an event is triggered here and is caught by background script. // 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({ const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTabUrl', action: 'getActiveTab',
}); });
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty // Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
const companyURL = extractCompanyLinkedinLink(activeTabUrl); const companyURL = extractCompanyLinkedinLink(activeTab.url);
companyInputData.linkedinLink = { url: companyURL, label: companyURL }; companyInputData.linkedinLink = { url: companyURL, label: companyURL };
const company = await fetchCompany({ const company = await createCompany(companyInputData);
linkedinLink: { return company;
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); export const insertButtonForCompany = async () => {
} else { const companyButtonDiv = createDefaultButton(
const newButtonCompany: HTMLDivElement = createNewButton( 'twenty-company-btn',
'Add to Twenty',
async () => { async () => {
const response = await createCompany(companyInputData); if (isDefined(companyButtonDiv)) {
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
if (response) { companyBtnSpan.textContent = 'Saving...';
newButtonCompany.textContent = 'Saved'; const company = await addCompany();
newButtonCompany.setAttribute('disabled', 'true'); if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
// Button specific styles once the button is unclickable after successfully sending data to server. Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
newButtonCompany.addEventListener('mouseenter', () => {
const hoverStyles = {
backgroundColor: 'black',
borderColor: 'black',
cursor: 'default',
};
Object.assign(newButtonCompany.style, hoverStyles);
});
} else { } else {
newButtonCompany.textContent = 'Try Again'; companyBtnSpan.textContent = 'Try again';
}
} }
}, },
); );
// Include the button in the DOM. const parentDiv: HTMLDivElement | null = document.querySelector(
parentDiv.prepend(newButtonCompany); '.org-top-card-primary-actions__inner',
);
// Write button specific styles here - common ones can be found in createButton.ts. if (isDefined(parentDiv)) {
const buttonSpecificStyles = { Object.assign(companyButtonDiv.style, {
alignSelf: 'end', marginLeft: '.8rem',
}; marginTop: '.4rem',
});
Object.assign(newButtonCompany.style, buttonSpecificStyles); parentDiv.prepend(companyButtonDiv);
} }
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
const company = await checkIfCompanyExists();
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
} else {
companyBtnSpan.textContent = 'Add to Twenty';
} }
}; };
export default insertButtonForCompany;

View File

@ -1,17 +1,34 @@
import createNewButton from '~/contentScript/createButton'; import { createDefaultButton } from '~/contentScript/createButton';
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName'; import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
import { createPerson, fetchPerson } from '~/db/person.db'; import { createPerson, fetchPerson } from '~/db/person.db';
import { PersonInput } from '~/db/types/person.types'; import { PersonInput } from '~/db/types/person.types';
import { isDefined } from '~/utils/isDefined';
const insertButtonForPerson = async (): Promise<void> => { export const checkIfPersonExists = async () => {
// Select the element in which to create the button. const { tab: activeTab } = await chrome.runtime.sendMessage({
const parentDiv: HTMLDivElement | null = document.querySelector( action: 'getActiveTab',
'.pv-top-card-v2-ctas', });
);
// Create the button with desired callback funciton to execute upon click. let activeTabUrl = '';
if (parentDiv) { if (isDefined(activeTab.url.endsWith('/'))) {
// Extract person-specific data from the DOM. 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 personNameElement = document.querySelector('.text-heading-xlarge');
const separatorElement = document.querySelector( const separatorElement = document.querySelector(
@ -53,75 +70,56 @@ const insertButtonForPerson = async (): Promise<void> => {
}; };
// Extract active tab url using chrome API - an event is triggered here and is caught by background script. // 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({ const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTabUrl', action: 'getActiveTab',
}); });
let activeTabUrl = '';
// Remove last slash from the URL for consistency when saving usernames. // Remove last slash from the URL for consistency when saving usernames.
if (activeTabUrl.endsWith('/')) { if (isDefined(activeTab.url.endsWith('/'))) {
activeTabUrl = activeTabUrl.slice(0, -1); activeTabUrl = activeTab.url.slice(0, -1);
} }
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl }; personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const person = await createPerson(personData);
return person;
};
const person = await fetchPerson({ export const insertButtonForPerson = async () => {
name: { const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
firstName: { eq: firstName }, if (isDefined(personButtonDiv)) {
lastName: { eq: lastName }, const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
}, personBtnSpan.textContent = 'Saving...';
linkedinLink: { url: { eq: activeTabUrl }, label: { eq: activeTabUrl } }, const person = await addPerson();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else {
personBtnSpan.textContent = 'Try again';
}
}
}); });
if (person) { if (isDefined(personButtonDiv)) {
const savedPerson: HTMLDivElement = createNewButton( const parentDiv: HTMLDivElement | null = document.querySelector(
'Saved', '.pv-top-card-v2-ctas',
async () => {},
); );
// Include the button in the DOM. if (isDefined(parentDiv)) {
parentDiv.prepend(savedPerson); Object.assign(personButtonDiv.style, {
marginRight: '.8rem',
// Write button specific styles here - common ones can be found in createButton.ts.
const buttonSpecificStyles = {
marginRight: '0.5em',
};
Object.assign(savedPerson.style, buttonSpecificStyles);
} 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);
}); });
parentDiv.prepend(personButtonDiv);
}
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
const person = await checkIfPersonExists();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else { } else {
newButtonPerson.textContent = 'Try Again'; personBtnSpan.textContent = 'Add to Twenty';
}
},
);
// 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);
} }
} }
}; };
export default insertButtonForPerson;

View File

@ -1,10 +1,13 @@
import insertButtonForCompany from '~/contentScript/extractCompanyProfile'; import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
import insertButtonForPerson from '~/contentScript/extractPersonProfile'; import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
// Inject buttons into the DOM when SPA is reloaded on the resource url. // 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/ // e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
// await insertButtonForCompany();
(async () => {
await insertButtonForCompany(); await insertButtonForCompany();
await insertButtonForPerson(); 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/. // 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. // 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. // 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/". // "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
const extractCompanyLinkedinLink = (activeTabUrl: string) => { const extractCompanyLinkedinLink = (activeTabUrl: string) => {
// Regular expression to match the company ID // Regular expression to match the company ID
@ -7,7 +10,7 @@ const extractCompanyLinkedinLink = (activeTabUrl: string) => {
// Extract the company ID using the regex // Extract the company ID using the regex
const match = activeTabUrl.match(regex); const match = activeTabUrl.match(regex);
if (match && match[1]) { if (isDefined(match) && isDefined(match[1])) {
const companyID = match[1]; const companyID = match[1];
const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`; const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
return cleanCompanyURL; return cleanCompanyURL;

View File

@ -6,33 +6,42 @@ import {
import { Company, CompanyFilterInput } from '~/generated/graphql'; import { Company, CompanyFilterInput } from '~/generated/graphql';
import { CREATE_COMPANY } from '~/graphql/company/mutations'; import { CREATE_COMPANY } from '~/graphql/company/mutations';
import { FIND_COMPANY } from '~/graphql/company/queries'; import { FIND_COMPANY } from '~/graphql/company/queries';
import { isDefined } from '~/utils/isDefined';
import { callMutation, callQuery } from '../utils/requestDb'; import { callMutation, callQuery } from '../utils/requestDb';
export const fetchCompany = async ( export const fetchCompany = async (
companyfilerInput: CompanyFilterInput, companyfilerInput: CompanyFilterInput,
): Promise<Company | null> => { ): Promise<Company | null> => {
try {
const data = await callQuery<FindCompanyResponse>(FIND_COMPANY, { const data = await callQuery<FindCompanyResponse>(FIND_COMPANY, {
filter: { filter: {
...companyfilerInput, ...companyfilerInput,
}, },
}); });
if (data?.companies.edges) { if (isDefined(data?.companies.edges)) {
return data?.companies.edges.length > 0 return data?.companies.edges.length > 0
? data?.companies.edges[0].node ? data?.companies.edges[0].node
: null; : null;
} }
return null; return null;
} catch (error) {
return null;
}
}; };
export const createCompany = async ( export const createCompany = async (
company: CompanyInput, company: CompanyInput,
): Promise<string | null> => { ): Promise<string | null> => {
try {
const data = await callMutation<CreateCompanyResponse>(CREATE_COMPANY, { const data = await callMutation<CreateCompanyResponse>(CREATE_COMPANY, {
input: company, input: company,
}); });
if (data) { if (isDefined(data)) {
return data.createCompany.id; return data.createCompany.id;
} }
return null; return null;
} catch (error) {
return null;
}
}; };

View File

@ -6,31 +6,40 @@ import {
import { Person, PersonFilterInput } from '~/generated/graphql'; import { Person, PersonFilterInput } from '~/generated/graphql';
import { CREATE_PERSON } from '~/graphql/person/mutations'; import { CREATE_PERSON } from '~/graphql/person/mutations';
import { FIND_PERSON } from '~/graphql/person/queries'; import { FIND_PERSON } from '~/graphql/person/queries';
import { isDefined } from '~/utils/isDefined';
import { callMutation, callQuery } from '../utils/requestDb'; import { callMutation, callQuery } from '../utils/requestDb';
export const fetchPerson = async ( export const fetchPerson = async (
personFilterData: PersonFilterInput, personFilterData: PersonFilterInput,
): Promise<Person | null> => { ): Promise<Person | null> => {
try {
const data = await callQuery<FindPersonResponse>(FIND_PERSON, { const data = await callQuery<FindPersonResponse>(FIND_PERSON, {
filter: { filter: {
...personFilterData, ...personFilterData,
}, },
}); });
if (data?.people.edges) { if (isDefined(data?.people.edges)) {
return data?.people.edges.length > 0 ? data?.people.edges[0].node : null; return data?.people.edges.length > 0 ? data?.people.edges[0].node : null;
} }
return null; return null;
} catch (error) {
return null;
}
}; };
export const createPerson = async ( export const createPerson = async (
person: PersonInput, person: PersonInput,
): Promise<string | null> => { ): Promise<string | null> => {
try {
const data = await callMutation<CreatePersonResponse>(CREATE_PERSON, { const data = await callMutation<CreatePersonResponse>(CREATE_PERSON, {
input: person, input: person,
}); });
if (data?.createPerson) { if (isDefined(data?.createPerson)) {
return data.createPerson.id; return data.createPerson.id;
} }
return null; return null;
} catch (error) {
return null;
}
}; };

View File

@ -5,6 +5,7 @@ import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/Button'; import { Button } from '@/ui/input/button/Button';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { Toggle } from '@/ui/input/components/Toggle'; import { Toggle } from '@/ui/input/components/Toggle';
import { isDefined } from '~/utils/isDefined';
const StyledContainer = styled.div<{ isToggleOn: boolean }>` const StyledContainer = styled.div<{ isToggleOn: boolean }>`
width: 400px; width: 400px;
@ -71,11 +72,11 @@ export const ApiKeyForm = () => {
const getState = async () => { const getState = async () => {
const localStorage = await chrome.storage.local.get(); const localStorage = await chrome.storage.local.get();
if (localStorage.apiKey) { if (isDefined(localStorage.apiKey)) {
setApiKey(localStorage.apiKey); setApiKey(localStorage.apiKey);
} }
if (localStorage.serverBaseUrl) { if (isDefined(localStorage.serverBaseUrl)) {
setShowSection(true); setShowSection(true);
setRoute(localStorage.serverBaseUrl); setRoute(localStorage.serverBaseUrl);
} }

View File

@ -2,6 +2,8 @@ import { useEffect, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { isDefined } from '~/utils/isDefined';
export type ToggleSize = 'small' | 'medium'; export type ToggleSize = 'small' | 'medium';
type ContainerProps = { type ContainerProps = {
@ -54,7 +56,7 @@ export const Toggle = ({
const handleChange = () => { const handleChange = () => {
setIsOn(!isOn); setIsOn(!isOn);
if (onChange) { if (isDefined(onChange)) {
onChange(!isOn); onChange(!isOn);
} }
}; };

View File

@ -14,7 +14,7 @@ const handleQueryParams = (inputData: { [x: string]: unknown }): string => {
result = result.concat(`${key}: ${quote}${inputData[key]}${quote}, `); 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; return result;
}; };

View File

@ -0,0 +1,4 @@
import { isNull, isUndefined } from '@sniptt/guards';
export const isDefined = <T>(value: T | null | undefined): value is T =>
!isUndefined(value) && !isNull(value);

View File

@ -8,7 +8,7 @@
"**/*.test.ts", "**/*.test.ts",
"**/*.spec.tsx", "**/*.spec.tsx",
"**/*.test.tsx", "**/*.test.tsx",
"jest.config.ts", "jest.config.ts"
], ],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
} }