-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #2413 - Building a chrome extension for twenty to store person…
…/company data into a workspace. (#3430) * build: create a new vite project for chrome extension * feat: configure theme per the frontend codebase for chrome extension * feat: inject the add to twenty button into linkedin profile page * feat: create the api key form ui and render it on the options page * feat: inject the add to twenty button into linkedin company page * feat: scrape required data from both the user profile and the company profile * refactor: move modules into options because it is the only page using react for now * fix: show add to twenty button without having to reload the single page application * fix: extract domain of the business website instead of scrapping the industry type * feat: store api key to local storage and open options page when trying to store data without setting a key * feat: send data to the backend upon click and store it to the database * fix: open options page upon clicking the extension icon * fix: update terminology from user to person to match the codebase convention * fix: adopt chrome extension to monorepo approach using nx and get the development server working * fix: update vite config for build command to work per the requirement * feat: add instructions in the readme file to install the extension for local testing * fix: move server base url to a dotenv file and replace the hard-coded url * feat: permit user to configure a custom route for the server from the options page * fix: fetch api key and route from local storage and display on options page to inform users of their choices * fix: move front base url to dotenv and replace the hard-coded url * fix: remove the trailing slash from person and company linkedin username * fix: improve code commenting to explain implementation somewhat better * ci: introduce a workflow to build chrome extension to ensure it can be published * fix: format files to display code in a consistent manner per the prettier configuration in codebase * fix: improve the commenting significantly to explain important and hard-to-understand parts of the code * fix: remove unused permissions from the manifest file for publishing to the chrome web store * Add nx * Fix vale --------- Co-authored-by: Charles Bochet <charles@twenty.com>
- Loading branch information
1 parent
a15128d
commit 1265dc7
Showing
59 changed files
with
2,103 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
name: CI Chrome Extension | ||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
jobs: | ||
chrome-extension-yarn-install: | ||
runs-on: ci-8-cores | ||
env: | ||
VITE_SERVER_BASE_URL: http://localhost:3000 | ||
VITE_FRONT_BASE_URL: http://localhost:3001 | ||
steps: | ||
- name: Cancel Previous Runs | ||
uses: styfle/cancel-workflow-action@0.11.0 | ||
with: | ||
access_token: ${{ github.token }} | ||
- uses: actions/checkout@v4 | ||
- name: Setup Node.js | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: "18" | ||
- name: Cache chrome extension node modules | ||
uses: actions/cache@v3 | ||
with: | ||
path: packages/twenty-chrome-extension/node_modules | ||
key: chrome-extension-node_modules-${{hashFiles('yarn.lock')}} | ||
restore-keys: chrome-extension-node_modules- | ||
- name: Cache root node modules | ||
uses: actions/cache@v3 | ||
with: | ||
path: node_modules | ||
key: root-node_modules-${{hashFiles('yarn.lock')}} | ||
restore-keys: root-node_modules- | ||
- name: Chrome Extension / Install Dependencies | ||
run: yarn | ||
chrome-extension-build: | ||
needs: chrome-extension-yarn-install | ||
runs-on: ubuntu-latest | ||
env: | ||
VITE_SERVER_BASE_URL: http://localhost:3000 | ||
VITE_FRONT_BASE_URL: http://localhost:3001 | ||
steps: | ||
- name: Cancel Previous Runs | ||
uses: styfle/cancel-workflow-action@0.11.0 | ||
with: | ||
access_token: ${{ github.token }} | ||
- uses: actions/checkout@v4 | ||
- name: Setup Node.js | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: "18" | ||
- name: Cache chrome extension node modules | ||
uses: actions/cache@v3 | ||
with: | ||
path: packages/twenty-chrome-extension/node_modules | ||
key: chrome-extension-node_modules-${{hashFiles('yarn.lock')}} | ||
restore-keys: chrome-extension-node_modules- | ||
- name: Cache root node modules | ||
uses: actions/cache@v3 | ||
with: | ||
path: node_modules | ||
key: root-node_modules-${{hashFiles('yarn.lock')}} | ||
restore-keys: root-node_modules- | ||
- name: Chrome Extension / Run build | ||
run: yarn nx build twenty-chrome-extension |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
VITE_SERVER_BASE_URL=http://localhost:3000 | ||
VITE_FRONT_BASE_URL=http://localhost:3001 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module.exports = { | ||
root: true, | ||
env: { browser: true, es2020: true }, | ||
extends: [ | ||
'eslint:recommended', | ||
'plugin:@typescript-eslint/recommended', | ||
'plugin:react-hooks/recommended', | ||
], | ||
ignorePatterns: ['dist', '.eslintrc.cjs'], | ||
parser: '@typescript-eslint/parser', | ||
plugins: ['react-refresh'], | ||
rules: { | ||
'react-refresh/only-export-components': [ | ||
'warn', | ||
{ allowConstantExport: true }, | ||
], | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# Twenty Chrome Extension. | ||
|
||
This extension allows you to save `company` and `people` information to your twenty workspace directly from LinkedIn. | ||
|
||
To install the extension in development mode with hmr (hot module reload), follow these steps. | ||
|
||
- STEP 1: Clone the repository and run `yarn install` in the root directory. | ||
- STEP 2: Once the dependencies installation succeeds, create a file with env variables by executing the following command in the root directory. | ||
|
||
``` | ||
cp ./packages/twenty-chrome-extension/.env.example ./packages/twenty-chrome-extension/.env | ||
``` | ||
|
||
- STEP 3: Now, execute the following command in the root directory to start up the development server on Port 3002. This will create a `dist` folder in `twenty-chrome-extension`. | ||
|
||
``` | ||
yarn nx start twenty-chrome-extension | ||
``` | ||
|
||
- STEP 4: Open Google Chrome and head to the extensions page by typing `chrome://extensions` in the address bar. | ||
|
||
<p align="center"> | ||
<img src="../twenty-chrome-extension/public/readme-images/01-img-one.png" width="600" /> | ||
</p> | ||
|
||
- STEP 5: Turn on the `Developer mode` from the top-right corner and click `Load unpacked`. | ||
|
||
<p align="center"> | ||
<img src="../twenty-chrome-extension/public/readme-images/02-img-two.png" width="600" /> | ||
</p> | ||
|
||
- STEP 6: Select the `dist` folder from `twenty-chrome-extension`. | ||
|
||
<p align="center"> | ||
<img src="../twenty-chrome-extension/public/readme-images/03-img-three.png" width="600" /> | ||
</p> | ||
|
||
- STEP 7: This opens up the `options` page, where you must enter your API key. | ||
|
||
<p align="center"> | ||
<img src="../twenty-chrome-extension/public/readme-images/04-img-four.png" width="600" /> | ||
</p> | ||
|
||
- STEP 8: Reload any LinkedIn page that you opened before installing the extension for seamless experience. | ||
- STEP 9: Visit any individual or company profile on LinkedIn and click the `Add to Twenty` button to test. | ||
|
||
<p align="center"> | ||
<img src="../twenty-chrome-extension/public/readme-images/05-img-five.png" width="600" /> | ||
</p> | ||
|
||
To install the extension in production mode without hmr (hot module reload), replace the command in STEP THREE with `yarn nx build twenty-chrome-extension`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Twenty</title> | ||
</head> | ||
<body> | ||
<div id="app"></div> | ||
<script type="module" src="/src/options/index.tsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "twenty-chrome-extension", | ||
"description": "", | ||
"private": true, | ||
"version": "0.0.1", | ||
"type": "module", | ||
"scripts": { | ||
"nx": "NX_DEFAULT_PROJECT=twenty-chrome-extension node ../../node_modules/nx/bin/nx.js", | ||
"start": "vite", | ||
"build": "tsc && vite build" | ||
}, | ||
"dependencies": { | ||
"@types/chrome": "^0.0.256" | ||
}, | ||
"devDependencies": { | ||
"@crxjs/vite-plugin": "^1.0.14" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+650 KB
packages/twenty-chrome-extension/public/readme-images/03-img-three.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.29 MB
packages/twenty-chrome-extension/public/readme-images/04-img-four.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { openOptionsPage } from './utils/openOptionsPage'; | ||
|
||
console.log('Background Script Works'); | ||
|
||
// Open options page programmatically in a new tab. | ||
chrome.runtime.onInstalled.addListener(function (details) { | ||
if (details.reason === 'install') { | ||
openOptionsPage(); | ||
} | ||
}); | ||
|
||
// Open options page when extension icon is clicked. | ||
chrome.action.onClicked.addListener(function () { | ||
openOptionsPage(); | ||
}); | ||
|
||
// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks. | ||
// 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/" | ||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | ||
if (tabs && tabs[0]) { | ||
const activeTabUrl: string | undefined = tabs[0].url; | ||
sendResponse({ url: activeTabUrl }); | ||
} | ||
}); | ||
break; | ||
case 'openOptionsPage': | ||
openOptionsPage(); | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
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) => { | ||
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)) { | ||
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' }); | ||
injectedTabs.add(tabId); | ||
} else if (!isDesiredRoute) { | ||
injectedTabs.delete(tabId); // Clear entry if navigated away from LinkedIn company page. | ||
} | ||
} | ||
}); |
5 changes: 5 additions & 0 deletions
5
packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const openOptionsPage = () => { | ||
chrome.runtime.openOptionsPage(); | ||
}; | ||
|
||
export { openOptionsPage }; |
57 changes: 57 additions & 0 deletions
57
packages/twenty-chrome-extension/src/contentScript/createButton.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
function createNewButton( | ||
text: string, | ||
onClickHandler: () => void, | ||
): HTMLButtonElement { | ||
const newButton: HTMLButtonElement = document.createElement('button'); | ||
newButton.textContent = text; | ||
|
||
// Write universal styles for the button | ||
const buttonStyles = { | ||
border: '1px solid black', | ||
borderRadius: '20px', | ||
backgroundColor: 'black', | ||
color: 'white', | ||
fontSize: '1.5rem', | ||
fontWeight: '600', | ||
padding: '0.45em 1em', | ||
width: '15rem', | ||
height: '32px', | ||
}; | ||
|
||
// Apply common styles to the button. | ||
Object.assign(newButton.style, buttonStyles); | ||
|
||
// Apply common styles to specifc states of a button. | ||
newButton.addEventListener('mouseenter', () => { | ||
const hoverStyles = { | ||
backgroundColor: '#5e5e5e', | ||
borderColor: '#5e5e5e', | ||
}; | ||
Object.assign(newButton.style, hoverStyles); | ||
}); | ||
|
||
newButton.addEventListener('mouseleave', () => { | ||
Object.assign(newButton.style, buttonStyles); | ||
}); | ||
|
||
// Handle the click event. | ||
newButton.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 (!apiKey) { | ||
chrome.runtime.sendMessage({ action: 'openOptionsPage' }); | ||
return; | ||
} | ||
|
||
// Update content during the resolution of the request. | ||
newButton.textContent = 'Saving...'; | ||
|
||
// Call the provided onClickHandler function to handle button click logic | ||
onClickHandler(); | ||
}); | ||
|
||
return newButton; | ||
} | ||
|
||
export default createNewButton; |
Oops, something went wrong.