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>
66
.github/workflows/ci-chrome-extension.yaml
vendored
Normal file
@ -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
|
4
.vscode/twenty.code-workspace
vendored
@ -4,6 +4,10 @@
|
||||
"name": "ROOT",
|
||||
"path": "../"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-chrome-extension",
|
||||
"path": "../packages/twenty-chrome-extension"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-docker",
|
||||
"path": "../packages/twenty-docker"
|
||||
|
@ -283,6 +283,7 @@
|
||||
"version": "0.2.1",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/twenty-chrome-extension",
|
||||
"packages/twenty-front",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-server",
|
||||
|
2
packages/twenty-chrome-extension/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_SERVER_BASE_URL=http://localhost:3000
|
||||
VITE_FRONT_BASE_URL=http://localhost:3001
|
18
packages/twenty-chrome-extension/.eslintrc.cjs
Normal file
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
24
packages/twenty-chrome-extension/.gitignore
vendored
Normal file
@ -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?
|
51
packages/twenty-chrome-extension/README.md
Normal file
@ -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`.
|
12
packages/twenty-chrome-extension/options.html
Normal file
@ -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>
|
18
packages/twenty-chrome-extension/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
BIN
packages/twenty-chrome-extension/public/logo/32-32.png
Normal file
After Width: | Height: | Size: 790 B |
After Width: | Height: | Size: 350 KiB |
After Width: | Height: | Size: 233 KiB |
After Width: | Height: | Size: 650 KiB |
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 830 KiB |
1
packages/twenty-chrome-extension/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
62
packages/twenty-chrome-extension/src/background/index.ts
Normal file
@ -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.
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
const openOptionsPage = () => {
|
||||
chrome.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
export { openOptionsPage };
|
@ -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;
|
@ -0,0 +1,106 @@
|
||||
import handleQueryParams from '../utils/handleQueryParams';
|
||||
import requestDb from '../utils/requestDb';
|
||||
import createNewButton from './createButton';
|
||||
import extractCompanyLinkedinLink from './utils/extractCompanyLinkedinLink';
|
||||
import extractDomain from './utils/extractDomain';
|
||||
|
||||
function insertButtonForCompany(): 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) {
|
||||
const newButtonCompany: HTMLButtonElement = createNewButton(
|
||||
'Add to Twenty',
|
||||
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 companyData = {
|
||||
name: companyName,
|
||||
domainName: domainName,
|
||||
address: address,
|
||||
employees: employees,
|
||||
linkedinLink: { url: '', label: '' },
|
||||
};
|
||||
|
||||
// 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',
|
||||
});
|
||||
|
||||
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
|
||||
const companyURL = extractCompanyLinkedinLink(activeTabUrl);
|
||||
companyData.linkedinLink = { url: companyURL, label: companyURL };
|
||||
|
||||
const query = `mutation CreateOneCompany { createCompany(data:{${handleQueryParams(
|
||||
companyData,
|
||||
)}}) {id} }`;
|
||||
|
||||
const response = await requestDb(query);
|
||||
|
||||
if (response.data) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default insertButtonForCompany;
|
@ -0,0 +1,119 @@
|
||||
import handleQueryParams from '../utils/handleQueryParams';
|
||||
import requestDb from '../utils/requestDb';
|
||||
import createNewButton from './createButton';
|
||||
import extractFirstAndLastName from './utils/extractFirstAndLastName';
|
||||
|
||||
function insertButtonForPerson(): void {
|
||||
// Select the element in which to create the button.
|
||||
const parentDiv: HTMLDivElement | null = document.querySelector(
|
||||
'.pv-top-card-v2-ctas',
|
||||
);
|
||||
|
||||
// Create the button with desired callback funciton to execute upon click.
|
||||
if (parentDiv) {
|
||||
const newButtonPerson: HTMLButtonElement = createNewButton(
|
||||
'Add to Twenty',
|
||||
async () => {
|
||||
// Extract person-specific data from the DOM.
|
||||
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',
|
||||
);
|
||||
|
||||
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 = {
|
||||
name: { firstName, lastName },
|
||||
city: personCity,
|
||||
avatarUrl: profilePicture,
|
||||
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);
|
||||
}
|
||||
|
||||
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
|
||||
|
||||
const query = `mutation CreateOnePerson { createPerson(data:{${handleQueryParams(
|
||||
personData,
|
||||
)}}) {id} }`;
|
||||
|
||||
const response = await requestDb(query);
|
||||
|
||||
if (response.data) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default insertButtonForPerson;
|
20
packages/twenty-chrome-extension/src/contentScript/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import insertButtonForPerson from './extractPersonProfile';
|
||||
import insertButtonForCompany from './extractCompanyProfile';
|
||||
|
||||
// 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/
|
||||
insertButtonForCompany();
|
||||
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.
|
||||
// Therefore, if the user navigates to a person or a company page, we must manually re-execute the content script to create the "Add to Twenty" button.
|
||||
// e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/
|
||||
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
||||
if (message.action === 'executeContentScript') {
|
||||
insertButtonForCompany();
|
||||
insertButtonForPerson();
|
||||
}
|
||||
|
||||
sendResponse('Executing!');
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
// Extract "https://www.linkedin.com/company/twenty/" from any of the following urls, which the user can visit while on the company page.
|
||||
// "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
|
||||
const regex = /\/company\/([^/]*)/;
|
||||
|
||||
// Extract the company ID using the regex
|
||||
const match = activeTabUrl.match(regex);
|
||||
|
||||
if (match && match[1]) {
|
||||
const companyID = match[1];
|
||||
const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
|
||||
return cleanCompanyURL;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export default extractCompanyLinkedinLink;
|
@ -0,0 +1,15 @@
|
||||
function extractDomain(url: string | null) {
|
||||
if (!url) return '';
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
let domain = hostname.replace('www.', '');
|
||||
|
||||
const parts = domain.split('.');
|
||||
if (parts.length > 2) {
|
||||
domain = parts.slice(1).join('.');
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
export default extractDomain;
|
@ -0,0 +1,9 @@
|
||||
// Separate first name and last name from a full name.
|
||||
const extractFirstAndLastName = (fullName: string) => {
|
||||
const spaceIndex = fullName.lastIndexOf(' ');
|
||||
const firstName = fullName.substring(0, spaceIndex);
|
||||
const lastName = fullName.substring(spaceIndex + 1);
|
||||
return { firstName, lastName };
|
||||
};
|
||||
|
||||
export default extractFirstAndLastName;
|
3
packages/twenty-chrome-extension/src/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
11
packages/twenty-chrome-extension/src/index.css
Normal file
@ -0,0 +1,11 @@
|
||||
* {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
36
packages/twenty-chrome-extension/src/manifest.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { defineManifest } from '@crxjs/vite-plugin';
|
||||
import packageData from '../package.json';
|
||||
|
||||
export default defineManifest({
|
||||
manifest_version: 3,
|
||||
name: 'Twenty',
|
||||
description: packageData.description,
|
||||
version: packageData.version,
|
||||
|
||||
icons: {
|
||||
16: 'logo/32-32.png',
|
||||
32: 'logo/32-32.png',
|
||||
48: 'logo/32-32.png',
|
||||
},
|
||||
|
||||
action: {},
|
||||
|
||||
options_page: 'options.html',
|
||||
|
||||
background: {
|
||||
service_worker: 'src/background/index.ts',
|
||||
type: 'module',
|
||||
},
|
||||
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ['https://www.linkedin.com/*'],
|
||||
js: ['src/contentScript/index.ts'],
|
||||
run_at: 'document_end',
|
||||
},
|
||||
],
|
||||
|
||||
permissions: ['activeTab', 'storage'],
|
||||
|
||||
host_permissions: ['https://www.linkedin.com/*'],
|
||||
});
|
21
packages/twenty-chrome-extension/src/options/Options.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ApiKeyForm } from './modules/api-key/components/ApiKeyForm';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const Options = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ApiKeyForm />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
19
packages/twenty-chrome-extension/src/options/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './Options';
|
||||
import '../index.css';
|
||||
import { AppThemeProvider } from './modules/ui/theme/components/AppThemeProvider';
|
||||
import { ThemeType } from './modules/ui/theme/constants/theme';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||
<AppThemeProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</AppThemeProvider>,
|
||||
);
|
||||
|
||||
declare module '@emotion/react' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { H2Title } from '../../ui/display/typography/components/H2Title';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { TextInput } from '../../ui/input/components/TextInput';
|
||||
import { Button } from '../../ui/input/button/Button';
|
||||
import { Toggle } from '../../ui/input/components/Toggle';
|
||||
|
||||
const StyledContainer = styled.div<{ isToggleOn: boolean }>`
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
padding: ${({ theme }) => theme.spacing(10)};
|
||||
overflow: hidden;
|
||||
transition: height 0.3s ease;
|
||||
|
||||
height: ${({ isToggleOn }) => (isToggleOn ? '450px' : '390px')};
|
||||
max-height: ${({ isToggleOn }) => (isToggleOn ? '450px' : '390px')};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.header`
|
||||
text-align: center;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledImg = styled.img``;
|
||||
|
||||
const StyledMain = styled.main`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.footer`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
flex: 0 0 80%;
|
||||
`;
|
||||
|
||||
const StyledToggleContainer = styled.div`
|
||||
flex: 0 0 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const StyledSection = styled.div<{ showSection: boolean }>`
|
||||
transition:
|
||||
max-height 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
overflow: hidden;
|
||||
max-height: ${({ showSection }) => (showSection ? '200px' : '0')};
|
||||
`;
|
||||
|
||||
export const ApiKeyForm = () => {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [route, setRoute] = useState('');
|
||||
const [showSection, setShowSection] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getState = async () => {
|
||||
const localStorage = await chrome.storage.local.get();
|
||||
|
||||
if (localStorage.apiKey) {
|
||||
setApiKey(localStorage.apiKey);
|
||||
}
|
||||
|
||||
if (localStorage.serverBaseUrl) {
|
||||
setRoute(localStorage.serverBaseUrl);
|
||||
}
|
||||
};
|
||||
|
||||
void getState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.set({ apiKey });
|
||||
}, [apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.set({ serverBaseUrl: route });
|
||||
}, [route]);
|
||||
|
||||
const handleGenerateClick = () => {
|
||||
window.open(
|
||||
`${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers/api-keys`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
setShowSection(!showSection);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer isToggleOn={showSection}>
|
||||
<StyledHeader>
|
||||
<StyledImg src="/logo/32-32.png" alt="Twenty Logo" />
|
||||
</StyledHeader>
|
||||
|
||||
<StyledMain>
|
||||
<H2Title
|
||||
title="Connect your account"
|
||||
description="Input your key to link the extension to your workspace."
|
||||
/>
|
||||
<TextInput
|
||||
label="Api key"
|
||||
value={apiKey}
|
||||
onChange={setApiKey}
|
||||
placeholder="My API key"
|
||||
/>
|
||||
<Button
|
||||
title="Generate a key"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
size="small"
|
||||
position="standalone"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
onClick={handleGenerateClick}
|
||||
/>
|
||||
</StyledMain>
|
||||
|
||||
<StyledFooter>
|
||||
<StyledTitleContainer>
|
||||
<H2Title
|
||||
title="Custom route"
|
||||
description="For developers interested in self-hosting or local testing of the extension."
|
||||
/>
|
||||
</StyledTitleContainer>
|
||||
<StyledToggleContainer>
|
||||
<Toggle value={showSection} onChange={handleToggle} />
|
||||
</StyledToggleContainer>
|
||||
</StyledFooter>
|
||||
|
||||
<StyledSection showSection={showSection}>
|
||||
{showSection && (
|
||||
<TextInput
|
||||
label="Route"
|
||||
value={route}
|
||||
onChange={setRoute}
|
||||
placeholder="My Route"
|
||||
/>
|
||||
)}
|
||||
</StyledSection>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type H2TitleProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
addornment?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin: 0;
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
|
||||
<StyledContainer>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{addornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
</StyledContainer>
|
||||
);
|
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type ButtonSize = 'medium' | 'small';
|
||||
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
||||
export type ButtonAccent = 'default' | 'blue' | 'danger';
|
||||
|
||||
export type ButtonProps = {
|
||||
className?: string;
|
||||
Icon?: React.ReactNode;
|
||||
title?: string;
|
||||
fullWidth?: boolean;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
position?: ButtonPosition;
|
||||
accent?: ButtonAccent;
|
||||
soon?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
const StyledButton = styled.button<ButtonProps>`
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${({ position, theme }) => {
|
||||
switch (position) {
|
||||
case 'left':
|
||||
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
|
||||
case 'right':
|
||||
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
|
||||
case 'middle':
|
||||
return '0px';
|
||||
case 'standalone':
|
||||
return theme.border.radius.sm;
|
||||
default:
|
||||
return theme.border.radius.sm;
|
||||
}
|
||||
}};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: 500;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Button = ({
|
||||
className,
|
||||
Icon,
|
||||
title,
|
||||
fullWidth = false,
|
||||
variant = 'primary',
|
||||
size = 'medium',
|
||||
position = 'standalone',
|
||||
soon = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
}: ButtonProps) => (
|
||||
<StyledButton
|
||||
fullWidth={fullWidth}
|
||||
variant={variant}
|
||||
size={size}
|
||||
position={position}
|
||||
disabled={soon || disabled}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{Icon && Icon}
|
||||
{title}
|
||||
{soon && 'Soon'}
|
||||
</StyledButton>
|
||||
);
|
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
interface TextInputProps {
|
||||
label?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
fullWidth?: boolean;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledErrorHelper = styled.div`
|
||||
color: #ff0000;
|
||||
font-size: 12px;
|
||||
padding: 5px 0;
|
||||
`;
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
fullWidth,
|
||||
error,
|
||||
placeholder,
|
||||
icon,
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer fullWidth={fullWidth}>
|
||||
{label && <StyledLabel>{label}</StyledLabel>}
|
||||
<StyledInputContainer>
|
||||
{icon && <StyledIcon>{icon}</StyledIcon>}
|
||||
<StyledInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { TextInput };
|
@ -0,0 +1,83 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
type ContainerProps = {
|
||||
isOn: boolean;
|
||||
color?: string;
|
||||
toggleSize: ToggleSize;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? color ?? theme.color.blue : theme.background.quaternary};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
||||
`;
|
||||
|
||||
const StyledCircle = styled(motion.div)<{
|
||||
toggleSize: ToggleSize;
|
||||
}>`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-radius: 50%;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
`;
|
||||
|
||||
export type ToggleProps = {
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
color?: string;
|
||||
toggleSize?: ToggleSize;
|
||||
};
|
||||
|
||||
export const Toggle = ({
|
||||
value,
|
||||
onChange,
|
||||
color,
|
||||
toggleSize = 'medium',
|
||||
}: ToggleProps) => {
|
||||
const [isOn, setIsOn] = useState(value ?? false);
|
||||
|
||||
const circleVariants = {
|
||||
on: { x: toggleSize === 'small' ? 10 : 14 },
|
||||
off: { x: 2 },
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
setIsOn(!isOn);
|
||||
|
||||
if (onChange) {
|
||||
onChange(!isOn);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== isOn) {
|
||||
setIsOn(value ?? false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={handleChange}
|
||||
isOn={isOn}
|
||||
color={color}
|
||||
toggleSize={toggleSize}
|
||||
>
|
||||
<StyledCircle
|
||||
animate={isOn ? 'on' : 'off'}
|
||||
variants={circleVariants}
|
||||
toggleSize={toggleSize}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,15 @@
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
|
||||
import { lightTheme } from '../constants/theme';
|
||||
|
||||
type AppThemeProviderProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const AppThemeProvider: React.FC<AppThemeProviderProps> = ({ children }) => {
|
||||
const theme = lightTheme;
|
||||
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
};
|
||||
|
||||
export { AppThemeProvider };
|
@ -0,0 +1,19 @@
|
||||
import { color } from './colors';
|
||||
|
||||
export const accentLight = {
|
||||
primary: color.blueAccent25,
|
||||
secondary: color.blueAccent20,
|
||||
tertiary: color.blueAccent15,
|
||||
quaternary: color.blueAccent10,
|
||||
accent3570: color.blueAccent35,
|
||||
accent4060: color.blueAccent40,
|
||||
};
|
||||
|
||||
export const accentDark = {
|
||||
primary: color.blueAccent75,
|
||||
secondary: color.blueAccent80,
|
||||
tertiary: color.blueAccent85,
|
||||
quaternary: color.blueAccent90,
|
||||
accent3570: color.blueAccent70,
|
||||
accent4060: color.blueAccent60,
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
export const animation = {
|
||||
duration: {
|
||||
instant: 0.075,
|
||||
fast: 0.15,
|
||||
normal: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
export type AnimationDuration = 'instant' | 'fast' | 'normal';
|
@ -0,0 +1,47 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
import DarkNoise from '../assets/dark-noise.jpg';
|
||||
import LightNoise from '../assets/light-noise.png';
|
||||
|
||||
import { color, grayScale, rgba } from './colors';
|
||||
|
||||
export const backgroundLight = {
|
||||
noisy: `url(${LightNoise.toString()});`,
|
||||
primary: grayScale.gray0,
|
||||
secondary: grayScale.gray10,
|
||||
tertiary: grayScale.gray15,
|
||||
quaternary: grayScale.gray20,
|
||||
danger: color.red10,
|
||||
transparent: {
|
||||
primary: rgba(grayScale.gray0, 0.8),
|
||||
secondary: rgba(grayScale.gray10, 0.8),
|
||||
strong: rgba(grayScale.gray100, 0.16),
|
||||
medium: rgba(grayScale.gray100, 0.08),
|
||||
light: rgba(grayScale.gray100, 0.04),
|
||||
lighter: rgba(grayScale.gray100, 0.02),
|
||||
danger: rgba(color.red, 0.08),
|
||||
},
|
||||
overlay: rgba(grayScale.gray80, 0.8),
|
||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
};
|
||||
|
||||
export const backgroundDark = {
|
||||
noisy: `url(${DarkNoise.toString()});`,
|
||||
primary: grayScale.gray85,
|
||||
secondary: grayScale.gray80,
|
||||
tertiary: grayScale.gray75,
|
||||
quaternary: grayScale.gray70,
|
||||
danger: color.red80,
|
||||
transparent: {
|
||||
primary: rgba(grayScale.gray85, 0.8),
|
||||
secondary: rgba(grayScale.gray80, 0.8),
|
||||
strong: rgba(grayScale.gray0, 0.14),
|
||||
medium: rgba(grayScale.gray0, 0.1),
|
||||
light: rgba(grayScale.gray0, 0.06),
|
||||
lighter: rgba(grayScale.gray0, 0.03),
|
||||
danger: rgba(color.red, 0.08),
|
||||
},
|
||||
overlay: rgba(grayScale.gray80, 0.8),
|
||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export const blur = {
|
||||
light: 'blur(6px)',
|
||||
strong: 'blur(20px)',
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { color, grayScale } from './colors';
|
||||
|
||||
const common = {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
rounded: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
export const borderLight = {
|
||||
color: {
|
||||
strong: grayScale.gray25,
|
||||
medium: grayScale.gray20,
|
||||
light: grayScale.gray15,
|
||||
secondaryInverted: grayScale.gray50,
|
||||
inverted: grayScale.gray60,
|
||||
danger: color.red20,
|
||||
},
|
||||
...common,
|
||||
};
|
||||
|
||||
export const borderDark = {
|
||||
color: {
|
||||
strong: grayScale.gray55,
|
||||
medium: grayScale.gray65,
|
||||
light: grayScale.gray70,
|
||||
secondaryInverted: grayScale.gray35,
|
||||
inverted: grayScale.gray20,
|
||||
danger: color.red70,
|
||||
},
|
||||
...common,
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { grayScale, rgba } from './colors';
|
||||
|
||||
export const boxShadowLight = {
|
||||
extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
|
||||
light: `0px 2px 4px 0px ${rgba(
|
||||
grayScale.gray100,
|
||||
0.04,
|
||||
)}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
|
||||
strong: `2px 4px 16px 0px ${rgba(
|
||||
grayScale.gray100,
|
||||
0.12,
|
||||
)}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.04)}`,
|
||||
underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
|
||||
};
|
||||
|
||||
export const boxShadowDark = {
|
||||
extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
|
||||
light: `0px 2px 4px 0px ${rgba(
|
||||
grayScale.gray100,
|
||||
0.04,
|
||||
)}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
|
||||
strong: `2px 4px 16px 0px ${rgba(
|
||||
grayScale.gray100,
|
||||
0.16,
|
||||
)}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
|
||||
underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
|
||||
};
|
@ -0,0 +1,153 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
import hexRgb from 'hex-rgb';
|
||||
|
||||
export const grayScale = {
|
||||
gray100: '#000000',
|
||||
gray90: '#141414',
|
||||
gray85: '#171717',
|
||||
gray80: '#1b1b1b',
|
||||
gray75: '#1d1d1d',
|
||||
gray70: '#222222',
|
||||
gray65: '#292929',
|
||||
gray60: '#333333',
|
||||
gray55: '#4c4c4c',
|
||||
gray50: '#666666',
|
||||
gray45: '#818181',
|
||||
gray40: '#999999',
|
||||
gray35: '#b3b3b3',
|
||||
gray30: '#cccccc',
|
||||
gray25: '#d6d6d6',
|
||||
gray20: '#ebebeb',
|
||||
gray15: '#f1f1f1',
|
||||
gray10: '#fcfcfc',
|
||||
gray0: '#ffffff',
|
||||
};
|
||||
|
||||
export const mainColors = {
|
||||
yellow: '#ffd338',
|
||||
green: '#55ef3c',
|
||||
turquoise: '#15de8f',
|
||||
sky: '#00e0ff',
|
||||
blue: '#1961ed',
|
||||
purple: '#915ffd',
|
||||
pink: '#f54bd0',
|
||||
red: '#f83e3e',
|
||||
orange: '#ff7222',
|
||||
gray: grayScale.gray30,
|
||||
};
|
||||
|
||||
export type ThemeColor = keyof typeof mainColors;
|
||||
|
||||
export const secondaryColors = {
|
||||
yellow80: '#2e2a1a',
|
||||
yellow70: '#453d1e',
|
||||
yellow60: '#746224',
|
||||
yellow50: '#b99b2e',
|
||||
yellow40: '#ffe074',
|
||||
yellow30: '#ffedaf',
|
||||
yellow20: '#fff6d7',
|
||||
yellow10: '#fffbeb',
|
||||
|
||||
green80: '#1d2d1b',
|
||||
green70: '#23421e',
|
||||
green60: '#2a5822',
|
||||
green50: '#42ae31',
|
||||
green40: '#88f477',
|
||||
green30: '#ccfac5',
|
||||
green20: '#ddfcd8',
|
||||
green10: '#eefdec',
|
||||
|
||||
turquoise80: '#172b23',
|
||||
turquoise70: '#173f2f',
|
||||
turquoise60: '#166747',
|
||||
turquoise50: '#16a26b',
|
||||
turquoise40: '#5be8b1',
|
||||
turquoise30: '#a1f2d2',
|
||||
turquoise20: '#d0f8e9',
|
||||
turquoise10: '#e8fcf4',
|
||||
|
||||
sky80: '#152b2e',
|
||||
sky70: '#123f45',
|
||||
sky60: '#0e6874',
|
||||
sky50: '#07a4b9',
|
||||
sky40: '#4de9ff',
|
||||
sky30: '#99f3ff',
|
||||
sky20: '#ccf9ff',
|
||||
sky10: '#e5fcff',
|
||||
|
||||
blue80: '#171e2c',
|
||||
blue70: '#172642',
|
||||
blue60: '#18356d',
|
||||
blue50: '#184bad',
|
||||
blue40: '#5e90f2',
|
||||
blue30: '#a3c0f8',
|
||||
blue20: '#d1dffb',
|
||||
blue10: '#e8effd',
|
||||
|
||||
purple80: '#231e2e',
|
||||
purple70: '#2f2545',
|
||||
purple60: '#483473',
|
||||
purple50: '#6c49b8',
|
||||
purple40: '#b28ffe',
|
||||
purple30: '#d3bffe',
|
||||
purple20: '#e9dfff',
|
||||
purple10: '#f4efff',
|
||||
|
||||
pink80: '#2d1c29',
|
||||
pink70: '#43213c',
|
||||
pink60: '#702c61',
|
||||
pink50: '#b23b98',
|
||||
pink40: '#f881de',
|
||||
pink30: '#fbb7ec',
|
||||
pink20: '#fddbf6',
|
||||
pink10: '#feedfa',
|
||||
|
||||
red80: '#2d1b1b',
|
||||
red70: '#441f1f',
|
||||
red60: '#712727',
|
||||
red50: '#b43232',
|
||||
red40: '#fa7878',
|
||||
red30: '#fcb2b2',
|
||||
red20: '#fed8d8',
|
||||
red10: '#feecec',
|
||||
|
||||
orange80: '#2e2018',
|
||||
orange70: '#452919',
|
||||
orange60: '#743b1b',
|
||||
orange50: '#b9571f',
|
||||
orange40: '#ff9c64',
|
||||
orange30: '#ffc7a7',
|
||||
orange20: '#ffe3d3',
|
||||
orange10: '#fff1e9',
|
||||
|
||||
gray80: grayScale.gray70,
|
||||
gray70: grayScale.gray65,
|
||||
gray60: grayScale.gray55,
|
||||
gray50: grayScale.gray40,
|
||||
gray40: grayScale.gray25,
|
||||
gray30: grayScale.gray20,
|
||||
gray20: grayScale.gray15,
|
||||
gray10: grayScale.gray10,
|
||||
blueAccent90: '#141a25',
|
||||
blueAccent85: '#151d2e',
|
||||
blueAccent80: '#152037',
|
||||
blueAccent75: '#16233f',
|
||||
blueAccent70: '#17294a',
|
||||
blueAccent60: '#18356d',
|
||||
blueAccent40: '#a3c0f8',
|
||||
blueAccent35: '#c8d9fb',
|
||||
blueAccent25: '#dae6fc',
|
||||
blueAccent20: '#e2ecfd',
|
||||
blueAccent15: '#edf2fe',
|
||||
blueAccent10: '#f5f9fd',
|
||||
};
|
||||
|
||||
export const color = {
|
||||
...mainColors,
|
||||
...secondaryColors,
|
||||
};
|
||||
|
||||
export const rgba = (hex: string, alpha: number) => {
|
||||
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
|
||||
return `rgba(${rgb},${alpha})`;
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { ThemeType } from './theme';
|
||||
|
||||
export const overlayBackground = (props: { theme: ThemeType }) =>
|
||||
css`
|
||||
backdrop-filter: blur(8px);
|
||||
background: ${props.theme.background.transparent.secondary};
|
||||
box-shadow: ${props.theme.boxShadow.strong};
|
||||
`;
|
||||
|
||||
export const textInputStyle = (props: { theme: ThemeType }) =>
|
||||
css`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${props.theme.font.color.primary};
|
||||
font-family: ${props.theme.font.family};
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
outline: none;
|
||||
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
|
||||
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${props.theme.font.color.light};
|
||||
font-family: ${props.theme.font.family};
|
||||
font-weight: ${props.theme.font.weight.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
export const hoverBackground = (props: any) =>
|
||||
css`
|
||||
transition: background 0.1s ease;
|
||||
&:hover {
|
||||
background: ${props.theme.background.transparent.light};
|
||||
}
|
||||
`;
|
@ -0,0 +1,45 @@
|
||||
import { color, grayScale } from './colors';
|
||||
|
||||
const common = {
|
||||
size: {
|
||||
xxs: '0.625rem',
|
||||
xs: '0.85rem',
|
||||
sm: '0.92rem',
|
||||
md: '1rem',
|
||||
lg: '1.23rem',
|
||||
xl: '1.54rem',
|
||||
xxl: '1.85rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semiBold: 600,
|
||||
},
|
||||
family: 'Inter, sans-serif',
|
||||
};
|
||||
|
||||
export const fontLight = {
|
||||
color: {
|
||||
primary: grayScale.gray60,
|
||||
secondary: grayScale.gray50,
|
||||
tertiary: grayScale.gray40,
|
||||
light: grayScale.gray35,
|
||||
extraLight: grayScale.gray30,
|
||||
inverted: grayScale.gray0,
|
||||
danger: color.red,
|
||||
},
|
||||
...common,
|
||||
};
|
||||
|
||||
export const fontDark = {
|
||||
color: {
|
||||
primary: grayScale.gray20,
|
||||
secondary: grayScale.gray35,
|
||||
tertiary: grayScale.gray45,
|
||||
light: grayScale.gray50,
|
||||
extraLight: grayScale.gray55,
|
||||
inverted: grayScale.gray100,
|
||||
danger: color.red,
|
||||
},
|
||||
...common,
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
export const icon = {
|
||||
size: {
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 40,
|
||||
},
|
||||
stroke: {
|
||||
sm: 1.6,
|
||||
md: 2,
|
||||
lg: 2.5,
|
||||
},
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export const modal = {
|
||||
size: {
|
||||
sm: '300px',
|
||||
md: '400px',
|
||||
lg: '53%',
|
||||
},
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
import { color } from './colors';
|
||||
|
||||
export const tagLight: { [key: string]: { [key: string]: string } } = {
|
||||
text: {
|
||||
green: color.green60,
|
||||
turquoise: color.turquoise60,
|
||||
sky: color.sky60,
|
||||
blue: color.blue60,
|
||||
purple: color.purple60,
|
||||
pink: color.pink60,
|
||||
red: color.red60,
|
||||
orange: color.orange60,
|
||||
yellow: color.yellow60,
|
||||
gray: color.gray60,
|
||||
},
|
||||
background: {
|
||||
green: color.green20,
|
||||
turquoise: color.turquoise20,
|
||||
sky: color.sky20,
|
||||
blue: color.blue20,
|
||||
purple: color.purple20,
|
||||
pink: color.pink20,
|
||||
red: color.red20,
|
||||
orange: color.orange20,
|
||||
yellow: color.yellow20,
|
||||
gray: color.gray20,
|
||||
},
|
||||
};
|
||||
|
||||
export const tagDark = {
|
||||
text: {
|
||||
green: color.green10,
|
||||
turquoise: color.turquoise10,
|
||||
sky: color.sky10,
|
||||
blue: color.blue10,
|
||||
purple: color.purple10,
|
||||
pink: color.pink10,
|
||||
red: color.red10,
|
||||
orange: color.orange10,
|
||||
yellow: color.yellow10,
|
||||
gray: color.gray10,
|
||||
},
|
||||
background: {
|
||||
green: color.green60,
|
||||
turquoise: color.turquoise60,
|
||||
sky: color.sky60,
|
||||
blue: color.blue60,
|
||||
purple: color.purple60,
|
||||
pink: color.pink60,
|
||||
red: color.red60,
|
||||
orange: color.orange60,
|
||||
yellow: color.yellow60,
|
||||
gray: color.gray60,
|
||||
},
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
export const text = {
|
||||
lineHeight: {
|
||||
lg: 1.5,
|
||||
md: 1.2,
|
||||
},
|
||||
|
||||
iconSizeMedium: 16,
|
||||
iconSizeSmall: 14,
|
||||
|
||||
iconStrikeLight: 1.6,
|
||||
iconStrikeMedium: 2,
|
||||
iconStrikeBold: 2.5,
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
import { accentDark, accentLight } from './accent';
|
||||
import { animation } from './animation';
|
||||
import { backgroundDark, backgroundLight } from './background';
|
||||
import { blur } from './blur';
|
||||
import { borderDark, borderLight } from './border';
|
||||
import { boxShadowDark, boxShadowLight } from './boxShadow';
|
||||
import { color, grayScale } from './colors';
|
||||
import { fontDark, fontLight } from './font';
|
||||
import { icon } from './icon';
|
||||
import { modal } from './modal';
|
||||
import { tagDark, tagLight } from './tag';
|
||||
import { text } from './text';
|
||||
|
||||
const common = {
|
||||
color: color,
|
||||
grayScale: grayScale,
|
||||
icon: icon,
|
||||
modal: modal,
|
||||
text: text,
|
||||
blur: blur,
|
||||
animation: animation,
|
||||
snackBar: {
|
||||
success: {
|
||||
background: '#16A26B',
|
||||
color: '#D0F8E9',
|
||||
},
|
||||
error: {
|
||||
background: '#B43232',
|
||||
color: '#FED8D8',
|
||||
},
|
||||
info: {
|
||||
background: color.gray80,
|
||||
color: grayScale.gray0,
|
||||
},
|
||||
},
|
||||
spacingMultiplicator: 4,
|
||||
spacing: (multiplicator: number) => `${multiplicator * 4}px`,
|
||||
betweenSiblingsGap: `2px`,
|
||||
table: {
|
||||
horizontalCellMargin: '8px',
|
||||
checkboxColumnWidth: '32px',
|
||||
},
|
||||
rightDrawerWidth: '500px',
|
||||
clickableElementBackgroundTransition: 'background 0.1s ease',
|
||||
lastLayerZIndex: 2147483647,
|
||||
};
|
||||
|
||||
export const lightTheme = {
|
||||
...common,
|
||||
...{
|
||||
accent: accentLight,
|
||||
background: backgroundLight,
|
||||
border: borderLight,
|
||||
tag: tagLight,
|
||||
boxShadow: boxShadowLight,
|
||||
font: fontLight,
|
||||
name: 'light',
|
||||
},
|
||||
};
|
||||
export type ThemeType = typeof lightTheme;
|
||||
|
||||
export const darkTheme: ThemeType = {
|
||||
...common,
|
||||
...{
|
||||
accent: accentDark,
|
||||
background: backgroundDark,
|
||||
border: borderDark,
|
||||
tag: tagDark,
|
||||
boxShadow: boxShadowDark,
|
||||
font: fontDark,
|
||||
name: 'dark',
|
||||
},
|
||||
};
|
||||
|
||||
export const MOBILE_VIEWPORT = 768;
|
@ -0,0 +1,21 @@
|
||||
// Convert extracted data into a structure that can be sent to the server for storage.
|
||||
const handleQueryParams = (inputData: { [x: string]: unknown }): string => {
|
||||
let result = '';
|
||||
Object.keys(inputData).forEach((key) => {
|
||||
let quote = '';
|
||||
if (typeof inputData[key] === 'string') quote = '"';
|
||||
if (typeof inputData[key] === 'object') {
|
||||
result = result.concat(
|
||||
`${key}: {${handleQueryParams(
|
||||
inputData[key] as { [x: string]: unknown },
|
||||
)}}, `,
|
||||
);
|
||||
} else {
|
||||
result = result.concat(`${key}: ${quote}${inputData[key]}${quote}, `);
|
||||
}
|
||||
});
|
||||
if (result.length) result = result.slice(0, -2); // Remove the last ', '
|
||||
return result;
|
||||
};
|
||||
|
||||
export default handleQueryParams;
|
29
packages/twenty-chrome-extension/src/utils/requestDb.ts
Normal file
@ -0,0 +1,29 @@
|
||||
const requestDb = async (query: string) => {
|
||||
const { apiKey } = await chrome.storage.local.get('apiKey');
|
||||
const { serverBaseUrl } = await chrome.storage.local.get('serverBaseUrl');
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${
|
||||
serverBaseUrl ? serverBaseUrl : import.meta.env.VITE_SERVER_BASE_URL
|
||||
}/graphql`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export default requestDb;
|
10
packages/twenty-chrome-extension/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER_BASE_URL: string;
|
||||
readonly VITE_FRONT_BASE_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
25
packages/twenty-chrome-extension/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
packages/twenty-chrome-extension/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "package.json", "src/*"]
|
||||
}
|
38
packages/twenty-chrome-extension/vite.config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { defineConfig, Plugin } from 'vite';
|
||||
import { crx } from '@crxjs/vite-plugin';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
import manifest from './src/manifest';
|
||||
|
||||
const viteManifestHack: Plugin & {
|
||||
renderCrxManifest: (manifest: unknown, bundle: unknown) => void;
|
||||
} = {
|
||||
// Workaround from https://github.com/crxjs/chrome-extension-tools/issues/846#issuecomment-1861880919.
|
||||
name: 'manifestHack',
|
||||
renderCrxManifest(_manifest, bundle) {
|
||||
bundle['manifest.json'] = bundle['.vite/manifest.json'];
|
||||
bundle['manifest.json'].fileName = 'manifest.json';
|
||||
delete bundle['.vite/manifest.json'];
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
output: { chunkFileNames: 'assets/chunk-[hash].js' },
|
||||
},
|
||||
},
|
||||
|
||||
// Adding this to fix websocket connection error.
|
||||
server: {
|
||||
port: 3002,
|
||||
strictPort: true,
|
||||
hmr: { port: 3002 },
|
||||
},
|
||||
|
||||
plugins: [viteManifestHack, crx({ manifest }), react()],
|
||||
};
|
||||
});
|
284
yarn.lock
@ -1544,6 +1544,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.23.5":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/core@npm:7.23.9"
|
||||
dependencies:
|
||||
"@ampproject/remapping": "npm:^2.2.0"
|
||||
"@babel/code-frame": "npm:^7.23.5"
|
||||
"@babel/generator": "npm:^7.23.6"
|
||||
"@babel/helper-compilation-targets": "npm:^7.23.6"
|
||||
"@babel/helper-module-transforms": "npm:^7.23.3"
|
||||
"@babel/helpers": "npm:^7.23.9"
|
||||
"@babel/parser": "npm:^7.23.9"
|
||||
"@babel/template": "npm:^7.23.9"
|
||||
"@babel/traverse": "npm:^7.23.9"
|
||||
"@babel/types": "npm:^7.23.9"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 03883300bf1252ab4c9ba5b52f161232dd52873dbe5cde9289bb2bb26e935c42682493acbac9194a59a3b6cbd17f4c4c84030db8d6d482588afe64531532ff9b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2":
|
||||
version: 7.23.6
|
||||
resolution: "@babel/generator@npm:7.23.6"
|
||||
@ -1858,6 +1881,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.23.9":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/helpers@npm:7.23.9"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.23.9"
|
||||
"@babel/traverse": "npm:^7.23.9"
|
||||
"@babel/types": "npm:^7.23.9"
|
||||
checksum: f69fd0aca96a6fb8bd6dd044cd8a5c0f1851072d4ce23355345b9493c4032e76d1217f86b70df795e127553cf7f3fcd1587ede9d1b03b95e8b62681ca2165b87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/highlight@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/highlight@npm:7.23.4"
|
||||
@ -1887,6 +1921,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.23.9":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/parser@npm:7.23.9"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 7df97386431366d4810538db4b9ec538f4377096f720c0591c7587a16f6810e62747e9fbbfa1ff99257fd4330035e4fb1b5b77c7bd3b97ce0d2e3780a6618975
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3"
|
||||
@ -2769,7 +2812,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@npm:^7.18.6":
|
||||
"@babel/plugin-transform-react-jsx-self@npm:^7.18.6, @babel/plugin-transform-react-jsx-self@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
|
||||
dependencies:
|
||||
@ -2780,7 +2823,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@npm:^7.19.6":
|
||||
"@babel/plugin-transform-react-jsx-source@npm:^7.19.6, @babel/plugin-transform-react-jsx-source@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3"
|
||||
dependencies:
|
||||
@ -3281,6 +3324,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.23.9":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/template@npm:7.23.9"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.23.5"
|
||||
"@babel/parser": "npm:^7.23.9"
|
||||
"@babel/types": "npm:^7.23.9"
|
||||
checksum: 0e8b60119433787742bc08ae762bbd8d6755611c4cabbcb7627b292ec901a55af65d93d1c88572326069efb64136ef151ec91ffb74b2df7689bbab237030833a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/traverse@npm:7.23.5"
|
||||
@ -3335,6 +3389,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.23.9":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/traverse@npm:7.23.9"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.23.5"
|
||||
"@babel/generator": "npm:^7.23.6"
|
||||
"@babel/helper-environment-visitor": "npm:^7.22.20"
|
||||
"@babel/helper-function-name": "npm:^7.23.0"
|
||||
"@babel/helper-hoist-variables": "npm:^7.22.5"
|
||||
"@babel/helper-split-export-declaration": "npm:^7.22.6"
|
||||
"@babel/parser": "npm:^7.23.9"
|
||||
"@babel/types": "npm:^7.23.9"
|
||||
debug: "npm:^4.3.1"
|
||||
globals: "npm:^11.1.0"
|
||||
checksum: d1615d1d02f04d47111a7ea4446a1a6275668ca39082f31d51f08380de9502e19862be434eaa34b022ce9a17dbb8f9e2b73a746c654d9575f3a680a7ffdf5630
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
version: 7.23.6
|
||||
resolution: "@babel/types@npm:7.23.6"
|
||||
@ -3357,6 +3429,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.23.9":
|
||||
version: 7.23.9
|
||||
resolution: "@babel/types@npm:7.23.9"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.23.4"
|
||||
"@babel/helper-validator-identifier": "npm:^7.22.20"
|
||||
to-fast-properties: "npm:^2.0.0"
|
||||
checksum: edc7bb180ce7e4d2aea10c6972fb10474341ac39ba8fdc4a27ffb328368dfdfbf40fca18e441bbe7c483774500d5c05e222cec276c242e952853dcaf4eb884f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@base2/pretty-print-object@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@base2/pretty-print-object@npm:1.0.1"
|
||||
@ -3873,6 +3956,34 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@crxjs/vite-plugin@npm:^1.0.14":
|
||||
version: 1.0.14
|
||||
resolution: "@crxjs/vite-plugin@npm:1.0.14"
|
||||
dependencies:
|
||||
"@rollup/pluginutils": "npm:^4.1.2"
|
||||
"@vitejs/plugin-react": "npm:>=1.2.0"
|
||||
"@webcomponents/custom-elements": "npm:^1.5.0"
|
||||
acorn-walk: "npm:^8.2.0"
|
||||
cheerio: "npm:^1.0.0-rc.10"
|
||||
connect-injector: "npm:^0.4.4"
|
||||
debug: "npm:^4.3.3"
|
||||
es-module-lexer: "npm:^0.10.0"
|
||||
fast-glob: "npm:^3.2.11"
|
||||
fs-extra: "npm:^10.0.1"
|
||||
jsesc: "npm:^3.0.2"
|
||||
magic-string: "npm:^0.26.0"
|
||||
picocolors: "npm:^1.0.0"
|
||||
react-refresh: "npm:^0.13.0"
|
||||
rollup: "npm:^2.70.2"
|
||||
peerDependencies:
|
||||
vite: ^2.9.0
|
||||
dependenciesMeta:
|
||||
"@vitejs/plugin-react":
|
||||
optional: true
|
||||
checksum: 19e203ddcfc3110973999bcc5224a0e8846b985a720d37ed55a945ed7ef726115bbb2ae06e5d30328c5c6338877acd1c77f27b35870fd92d2493b9bee65421f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cspotcode/source-map-support@npm:^0.8.0":
|
||||
version: 0.8.1
|
||||
resolution: "@cspotcode/source-map-support@npm:0.8.1"
|
||||
@ -10786,6 +10897,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/pluginutils@npm:^4.1.2":
|
||||
version: 4.2.1
|
||||
resolution: "@rollup/pluginutils@npm:4.2.1"
|
||||
dependencies:
|
||||
estree-walker: "npm:^2.0.1"
|
||||
picomatch: "npm:^2.2.2"
|
||||
checksum: 3ee56b2c8f1ed8dfd0a92631da1af3a2dfdd0321948f089b3752b4de1b54dc5076701eadd0e5fc18bd191b77af594ac1db6279e83951238ba16bf8a414c64c48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.0.5":
|
||||
version: 5.1.0
|
||||
resolution: "@rollup/pluginutils@npm:5.1.0"
|
||||
@ -14530,7 +14651,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0":
|
||||
"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.5":
|
||||
version: 7.20.5
|
||||
resolution: "@types/babel__core@npm:7.20.5"
|
||||
dependencies:
|
||||
@ -14644,6 +14765,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/chrome@npm:^0.0.256":
|
||||
version: 0.0.256
|
||||
resolution: "@types/chrome@npm:0.0.256"
|
||||
dependencies:
|
||||
"@types/filesystem": "npm:*"
|
||||
"@types/har-format": "npm:*"
|
||||
checksum: 35b3d2c92a3888cc14e5961421233003407a95078bf9b2f30c52a90470dae02588560bff1733ed3e7a8e9f12a1d0c5a6bae0ca30b6acdb3d723e1c2f29c8e861
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/codemirror@npm:^0.0.90":
|
||||
version: 0.0.90
|
||||
resolution: "@types/codemirror@npm:0.0.90"
|
||||
@ -14959,6 +15090,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/filesystem@npm:*":
|
||||
version: 0.0.35
|
||||
resolution: "@types/filesystem@npm:0.0.35"
|
||||
dependencies:
|
||||
"@types/filewriter": "npm:*"
|
||||
checksum: 16a380e9774c5a9e1358f3ee28a3d85a93488443f235d160da3969aae7858dc6c6148cb3ff6b7e814f1c43f17940da0941f004373566d4fe7f75d9fda5efe246
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/filewriter@npm:*":
|
||||
version: 0.0.32
|
||||
resolution: "@types/filewriter@npm:0.0.32"
|
||||
checksum: 4feea7890d7945059f8eec0f89b1a4fe4f0522156c9345d9123c3498c6dba4584a17bd886daa4392a2e19bd9d16ee82aff9a0e1b837af507b612bcc6bd4c4305
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/find-cache-dir@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "@types/find-cache-dir@npm:3.2.1"
|
||||
@ -15013,7 +15160,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/har-format@npm:^1.2.10":
|
||||
"@types/har-format@npm:*, @types/har-format@npm:^1.2.10":
|
||||
version: 1.2.15
|
||||
resolution: "@types/har-format@npm:1.2.15"
|
||||
checksum: 43ede55c3947e5cf59561f165930dc2eb3ae983fd162980cdc7274be1e7f528a6f77e65fee9a02a20d91b09bde10bed832b0470724f5c744ef6669015d00564e
|
||||
@ -16361,6 +16508,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:>=1.2.0":
|
||||
version: 4.2.1
|
||||
resolution: "@vitejs/plugin-react@npm:4.2.1"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.23.5"
|
||||
"@babel/plugin-transform-react-jsx-self": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-react-jsx-source": "npm:^7.23.3"
|
||||
"@types/babel__core": "npm:^7.20.5"
|
||||
react-refresh: "npm:^0.14.0"
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0
|
||||
checksum: de1eec44d703f32e5b58e776328ca20793657fe991835d15b290230b19a2a08be5d31501d424279ae13ecfed28044c117b69d746891c8d9b92c69e8a8907e989
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "@vitejs/plugin-react@npm:3.1.0"
|
||||
@ -16558,6 +16720,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webcomponents/custom-elements@npm:^1.5.0":
|
||||
version: 1.6.0
|
||||
resolution: "@webcomponents/custom-elements@npm:1.6.0"
|
||||
checksum: 8c3c3b0250ad7b063fe92b550fb725cc6074c8c5caea4a80901f9d9a93cdacf6dc0c73f715fa7b16f86e2ca1630e43cd80499bbf80e3a9b5c6ec042e074d22b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@whatwg-node/events@npm:^0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "@whatwg-node/events@npm:0.0.3"
|
||||
@ -16854,7 +17023,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1":
|
||||
"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0":
|
||||
version: 8.3.1
|
||||
resolution: "acorn-walk@npm:8.3.1"
|
||||
checksum: a23d2f7c6b6cad617f4c77f14dfeb062a239208d61753e9ba808d916c550add92b39535467d2e6028280761ac4f5a904cc9df21530b84d3f834e3edef74ddde5
|
||||
@ -20273,7 +20442,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cheerio@npm:^1.0.0-rc.12":
|
||||
"cheerio@npm:^1.0.0-rc.10, cheerio@npm:^1.0.0-rc.12":
|
||||
version: 1.0.0-rc.12
|
||||
resolution: "cheerio@npm:1.0.0-rc.12"
|
||||
dependencies:
|
||||
@ -21230,6 +21399,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"connect-injector@npm:^0.4.4":
|
||||
version: 0.4.4
|
||||
resolution: "connect-injector@npm:0.4.4"
|
||||
dependencies:
|
||||
debug: "npm:^2.0.0"
|
||||
q: "npm:^1.0.1"
|
||||
stream-buffers: "npm:^0.2.3"
|
||||
uberproto: "npm:^1.1.0"
|
||||
checksum: 6186a21285db6e989d610e7e2223305f59f1f11d4977bbf62db21680eb6b2f9b6080d5f03171d98d346496388c0cdb3f38bcec9bfebd3a58bc258ce12186ea1c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"consola@npm:^2.15.0, consola@npm:^2.15.3":
|
||||
version: 2.15.3
|
||||
resolution: "consola@npm:2.15.3"
|
||||
@ -22314,7 +22495,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9":
|
||||
"debug@npm:2.6.9, debug@npm:^2.0.0, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9":
|
||||
version: 2.6.9
|
||||
resolution: "debug@npm:2.6.9"
|
||||
dependencies:
|
||||
@ -23497,6 +23678,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-module-lexer@npm:^0.10.0":
|
||||
version: 0.10.5
|
||||
resolution: "es-module-lexer@npm:0.10.5"
|
||||
checksum: 5a199242971341fefe12ce5984602698d8f9c477e207f847aaed0f70519cf2c68ddbd22dd92b2cc4669a9d421a3b89a67d371994b64604ea24da21d35c42089e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-module-lexer@npm:^0.9.3":
|
||||
version: 0.9.3
|
||||
resolution: "es-module-lexer@npm:0.9.3"
|
||||
@ -24339,7 +24527,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"estree-walker@npm:^2.0.2":
|
||||
"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "estree-walker@npm:2.0.2"
|
||||
checksum: 53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af
|
||||
@ -25549,7 +25737,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0":
|
||||
"fs-extra@npm:^10.0.0, fs-extra@npm:^10.0.1, fs-extra@npm:^10.1.0":
|
||||
version: 10.1.0
|
||||
resolution: "fs-extra@npm:10.1.0"
|
||||
dependencies:
|
||||
@ -30290,6 +30478,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "jsesc@npm:3.0.2"
|
||||
bin:
|
||||
jsesc: bin/jsesc
|
||||
checksum: ef22148f9e793180b14d8a145ee6f9f60f301abf443288117b4b6c53d0ecd58354898dc506ccbb553a5f7827965cd38bc5fb726575aae93c5e8915e2de8290e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:~0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "jsesc@npm:0.5.0"
|
||||
@ -31538,6 +31735,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.26.0":
|
||||
version: 0.26.7
|
||||
resolution: "magic-string@npm:0.26.7"
|
||||
dependencies:
|
||||
sourcemap-codec: "npm:^1.4.8"
|
||||
checksum: 950035b344fe2a8163668980bc4a215a0b225086e6e22100fd947e7647053c6ba6b4f11a04de83a97a276526ccb602ef53b173725dbb1971fb146cff5a5e14f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.27.0":
|
||||
version: 0.27.0
|
||||
resolution: "magic-string@npm:0.27.0"
|
||||
@ -36647,7 +36853,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1":
|
||||
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "picomatch@npm:2.3.1"
|
||||
checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
|
||||
@ -38017,6 +38223,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"q@npm:^1.0.1":
|
||||
version: 1.5.1
|
||||
resolution: "q@npm:1.5.1"
|
||||
checksum: 7855fbdba126cb7e92ef3a16b47ba998c0786ec7fface236e3eb0135b65df36429d91a86b1fff3ab0927b4ac4ee88a2c44527c7c3b8e2a37efbec9fe34803df4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:6.11.0":
|
||||
version: 6.11.0
|
||||
resolution: "qs@npm:6.11.0"
|
||||
@ -38622,6 +38835,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-refresh@npm:^0.13.0":
|
||||
version: 0.13.0
|
||||
resolution: "react-refresh@npm:0.13.0"
|
||||
checksum: cb9f324d471485e569628854dc08d1550c0798cde57f1bfb8d954e006659de1da0bdccaf7d5d2ac0d3d53df1aae7b740b2a36128789afb8aff0f7ec01b128587
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-refresh@npm:^0.14.0":
|
||||
version: 0.14.0
|
||||
resolution: "react-refresh@npm:0.14.0"
|
||||
@ -40100,6 +40320,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup@npm:^2.70.2":
|
||||
version: 2.79.1
|
||||
resolution: "rollup@npm:2.79.1"
|
||||
dependencies:
|
||||
fsevents: "npm:~2.3.2"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
rollup: dist/bin/rollup
|
||||
checksum: 421418687f5dcd7324f4387f203c6bfc7118b7ace789e30f5da022471c43e037a76f5fd93837052754eeeae798a4fb266ac05ccee1e594406d912a59af98dde9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup@npm:^4.0.2":
|
||||
version: 4.9.2
|
||||
resolution: "rollup@npm:4.9.2"
|
||||
@ -41177,6 +41411,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sourcemap-codec@npm:^1.4.8":
|
||||
version: 1.4.8
|
||||
resolution: "sourcemap-codec@npm:1.4.8"
|
||||
checksum: f099279fdaae070ff156df7414bbe39aad69cdd615454947ed3e19136bfdfcb4544952685ee73f56e17038f4578091e12b17b283ed8ac013882916594d95b9e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"space-separated-tokens@npm:^1.0.0":
|
||||
version: 1.1.5
|
||||
resolution: "space-separated-tokens@npm:1.1.5"
|
||||
@ -41541,6 +41782,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream-buffers@npm:^0.2.3":
|
||||
version: 0.2.6
|
||||
resolution: "stream-buffers@npm:0.2.6"
|
||||
checksum: 8d685a5f98e0b392802fc07617f31e6ae63652ed2fff7fe7df309222ffb06502f47b31ab35c2cf9b4de0320f657ed3aa6d697641f0f72f5c6f3a703ba8d7b594
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream-combiner2@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "stream-combiner2@npm:1.1.1"
|
||||
@ -43179,6 +43427,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-chrome-extension@workspace:packages/twenty-chrome-extension":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "twenty-chrome-extension@workspace:packages/twenty-chrome-extension"
|
||||
dependencies:
|
||||
"@crxjs/vite-plugin": "npm:^1.0.14"
|
||||
"@types/chrome": "npm:^0.0.256"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"twenty-docs@workspace:packages/twenty-docs":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "twenty-docs@workspace:packages/twenty-docs"
|
||||
@ -43893,6 +44150,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uberproto@npm:^1.1.0":
|
||||
version: 1.2.0
|
||||
resolution: "uberproto@npm:1.2.0"
|
||||
checksum: 0071dbc7b3b71b4fedd4de5c914a6851df8ce11f6d98cf84ef8a1973afd8562027d111db97c047e2e42894bd5f99b24c6d07058d338d3204b3aea2c3c75421d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5":
|
||||
version: 1.0.6
|
||||
resolution: "uc.micro@npm:1.0.6"
|
||||
|