diff --git a/packages/twenty-chrome-extension/options.html b/packages/twenty-chrome-extension/page-inaccessible.html similarity index 76% rename from packages/twenty-chrome-extension/options.html rename to packages/twenty-chrome-extension/page-inaccessible.html index f36fb4472..4db64bcd2 100644 --- a/packages/twenty-chrome-extension/options.html +++ b/packages/twenty-chrome-extension/page-inaccessible.html @@ -7,6 +7,6 @@
- + - + \ No newline at end of file diff --git a/packages/twenty-chrome-extension/sidepanel.html b/packages/twenty-chrome-extension/sidepanel.html new file mode 100644 index 000000000..00088db93 --- /dev/null +++ b/packages/twenty-chrome-extension/sidepanel.html @@ -0,0 +1,22 @@ + + + + + + Twenty + + + + +
+ + + \ No newline at end of file diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts index f87f647c8..69ae83cc2 100644 --- a/packages/twenty-chrome-extension/src/background/index.ts +++ b/packages/twenty-chrome-extension/src/background/index.ts @@ -1,40 +1,55 @@ import Crypto from 'crypto-js'; -import { openOptionsPage } from '~/background/utils/openOptionsPage'; import { exchangeAuthorizationCode } from '~/db/auth.db'; import { isDefined } from '~/utils/isDefined'; // Open options page programmatically in a new tab. -chrome.runtime.onInstalled.addListener((details) => { - if (details.reason === 'install') { - openOptionsPage(); - } -}); +// chrome.runtime.onInstalled.addListener((details) => { +// if (details.reason === 'install') { +// openOptionsPage(); +// } +// }); -// Open options page when extension icon is clicked. -chrome.action.onClicked.addListener((tab) => { - chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' }); -}); +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); // 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 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/" - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (isDefined(tabs) && isDefined(tabs[0])) { - sendResponse({ tab: tabs[0] }); + case 'getActiveTab': { + // e.g. "https://linkedin.com/company/twenty/" + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (isDefined(tab) && isDefined(tab.id)) { + sendResponse({ tab }); } }); break; - case 'openOptionsPage': - openOptionsPage(); - break; - case 'CONNECT': + } + case 'launchOAuth': { launchOAuth(({ status, message }) => { sendResponse({ status, message }); }); break; + } + case 'openSidepanel': { + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (isDefined(tab) && isDefined(tab.id)) { + chrome.sidePanel.open({ tabId: tab.id }); + } + }); + break; + } + case 'changeSidepanelUrl': { + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (isDefined(tab) && isDefined(tab.id)) { + chrome.tabs.sendMessage(tab.id, { + action: 'changeSidepanelUrl', + message, + }); + } + }); + break; + } default: break; } @@ -101,13 +116,16 @@ const launchOAuth = ( callback({ status: true, message: '' }); - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (isDefined(tabs) && isDefined(tabs[0])) { - chrome.tabs.sendMessage(tabs[0].id ?? 0, { - action: 'AUTHENTICATED', - }); - } - }); + chrome.tabs.query( + { active: true, currentWindow: true }, + ([tab]) => { + if (isDefined(tab) && isDefined(tab.id)) { + chrome.tabs.sendMessage(tab.id, { + action: 'executeContentScript', + }); + } + }, + ); } }); } @@ -117,14 +135,22 @@ const launchOAuth = ( }); }; -chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { +chrome.tabs.onUpdated.addListener(async (tabId, _, 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 (tab.active === true) { if (isDefined(isDesiredRoute)) { chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' }); } } + + await chrome.sidePanel.setOptions({ + tabId, + path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/) + ? 'sidepanel.html' + : 'page-inaccessible.html', + enabled: true, + }); }); diff --git a/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts b/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts deleted file mode 100644 index a86b5fd66..000000000 --- a/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts +++ /dev/null @@ -1,5 +0,0 @@ -const openOptionsPage = () => { - chrome.runtime.openOptionsPage(); -}; - -export { openOptionsPage }; diff --git a/packages/twenty-chrome-extension/src/contentScript/createButton.ts b/packages/twenty-chrome-extension/src/contentScript/createButton.ts index a6883b955..d86418f0d 100644 --- a/packages/twenty-chrome-extension/src/contentScript/createButton.ts +++ b/packages/twenty-chrome-extension/src/contentScript/createButton.ts @@ -1,13 +1,16 @@ import { isDefined } from '~/utils/isDefined'; +interface CustomDiv extends HTMLDivElement { + onClickHandler: (newHandler: () => void) => void; +} + export const createDefaultButton = ( buttonId: string, - onClickHandler?: () => void, buttonText = '', -) => { - const btn = document.getElementById(buttonId); +): CustomDiv => { + const btn = document.getElementById(buttonId) as CustomDiv; if (isDefined(btn)) return btn; - const div = document.createElement('div'); + const div = document.createElement('div') as CustomDiv; const img = document.createElement('img'); const span = document.createElement('span'); @@ -52,19 +55,18 @@ export const createDefaultButton = ( Object.assign(div.style, divStyles); }); - // Handle the click event. - div.addEventListener('click', async (e) => { - e.preventDefault(); - const store = await chrome.storage.local.get(); + div.onClickHandler = (newHandler) => { + div.onclick = async () => { + const store = await chrome.storage.local.get(); - // If an api key is not set, the options page opens up to allow the user to configure an api key. - if (!store.accessToken) { - chrome.runtime.sendMessage({ action: 'openOptionsPage' }); - return; - } - - onClickHandler?.(); - }); + // If an api key is not set, the options page opens up to allow the user to configure an api key. + if (!store.accessToken) { + chrome.runtime.sendMessage({ action: 'openSidepanel' }); + return; + } + newHandler(); + }; + }; div.id = buttonId; diff --git a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts index 7b59566c4..12d6cc7b5 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts @@ -1,4 +1,5 @@ import { createDefaultButton } from '~/contentScript/createButton'; +import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl'; import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink'; import extractDomain from '~/contentScript/utils/extractDomain'; import { createCompany, fetchCompany } from '~/db/company.db'; @@ -71,27 +72,19 @@ export const addCompany = async () => { const companyURL = extractCompanyLinkedinLink(activeTab.url); companyInputData.linkedinLink = { url: companyURL, label: companyURL }; - const company = await createCompany(companyInputData); - return company; + const companyId = await createCompany(companyInputData); + + if (isDefined(companyId)) { + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`, + ); + } + + return companyId; }; export const insertButtonForCompany = async () => { - const companyButtonDiv = createDefaultButton( - 'twenty-company-btn', - async () => { - if (isDefined(companyButtonDiv)) { - const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0]; - companyBtnSpan.textContent = 'Saving...'; - const company = await addCompany(); - if (isDefined(company)) { - companyBtnSpan.textContent = 'Saved'; - Object.assign(companyButtonDiv.style, { pointerEvents: 'none' }); - } else { - companyBtnSpan.textContent = 'Try again'; - } - } - }, - ); + const companyButtonDiv = createDefaultButton('twenty-company-btn'); const parentDiv: HTMLDivElement | null = document.querySelector( '.org-top-card-primary-actions__inner', @@ -105,13 +98,35 @@ export const insertButtonForCompany = async () => { parentDiv.prepend(companyButtonDiv); } - const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0]; + const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0]; const company = await checkIfCompanyExists(); + const openCompanyOnSidePanel = (companyId: string) => { + companyButtonSpan.textContent = 'View in Twenty'; + companyButtonDiv.onClickHandler(async () => { + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`, + ); + chrome.runtime.sendMessage({ action: 'openSidepanel' }); + }); + }; + if (isDefined(company)) { - companyBtnSpan.textContent = 'Saved'; - Object.assign(companyButtonDiv.style, { pointerEvents: 'none' }); + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`, + ); + if (isDefined(company.id)) openCompanyOnSidePanel(company.id); } else { - companyBtnSpan.textContent = 'Add to Twenty'; + companyButtonSpan.textContent = 'Add to Twenty'; + + companyButtonDiv.onClickHandler(async () => { + companyButtonSpan.textContent = 'Saving...'; + const companyId = await addCompany(); + if (isDefined(companyId)) { + openCompanyOnSidePanel(companyId); + } else { + companyButtonSpan.textContent = 'Try again'; + } + }); } }; diff --git a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts index 0fb36d0b1..044745a07 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts @@ -1,4 +1,5 @@ import { createDefaultButton } from '~/contentScript/createButton'; +import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl'; import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName'; import { createPerson, fetchPerson } from '~/db/person.db'; import { PersonInput } from '~/db/types/person.types'; @@ -82,44 +83,58 @@ export const addPerson = async () => { } personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl }; - const person = await createPerson(personData); - return person; + const personId = await createPerson(personData); + + if (isDefined(personId)) { + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`, + ); + } + + return personId; }; export const insertButtonForPerson = async () => { - const personButtonDiv = createDefaultButton('twenty-person-btn', async () => { - if (isDefined(personButtonDiv)) { - const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0]; - personBtnSpan.textContent = 'Saving...'; - const person = await addPerson(); - if (isDefined(person)) { - personBtnSpan.textContent = 'Saved'; - Object.assign(personButtonDiv.style, { pointerEvents: 'none' }); - } else { - personBtnSpan.textContent = 'Try again'; - } - } - }); + const personButtonDiv = createDefaultButton('twenty-person-btn'); if (isDefined(personButtonDiv)) { - const parentDiv: HTMLDivElement | null = document.querySelector( - '.pv-top-card-v2-ctas', + const addedProfileDiv: HTMLDivElement | null = document.querySelector( + '.pv-top-card-v2-ctas__custom', ); - if (isDefined(parentDiv)) { + if (isDefined(addedProfileDiv)) { Object.assign(personButtonDiv.style, { marginRight: '.8rem', }); - parentDiv.prepend(personButtonDiv); + addedProfileDiv.prepend(personButtonDiv); } - const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0]; + const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0]; const person = await checkIfPersonExists(); + + const openPersonOnSidePanel = (personId: string) => { + personButtonSpan.textContent = 'View in Twenty'; + personButtonDiv.onClickHandler(async () => { + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`, + ); + chrome.runtime.sendMessage({ action: 'openSidepanel' }); + }); + }; + if (isDefined(person)) { - personBtnSpan.textContent = 'Saved'; - Object.assign(personButtonDiv.style, { pointerEvents: 'none' }); + await changeSidePanelUrl( + `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`, + ); + if (isDefined(person.id)) openPersonOnSidePanel(person.id); } else { - personBtnSpan.textContent = 'Add to Twenty'; + personButtonSpan.textContent = 'Add to Twenty'; + personButtonDiv.onClickHandler(async () => { + personButtonSpan.textContent = 'Saving...'; + const personId = await addPerson(); + if (isDefined(personId)) openPersonOnSidePanel(personId); + else personButtonSpan.textContent = 'Try again'; + }); } } }; diff --git a/packages/twenty-chrome-extension/src/contentScript/index.ts b/packages/twenty-chrome-extension/src/contentScript/index.ts index cc761248b..2699d3a1f 100644 --- a/packages/twenty-chrome-extension/src/contentScript/index.ts +++ b/packages/twenty-chrome-extension/src/contentScript/index.ts @@ -1,6 +1,5 @@ import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile'; import { insertButtonForPerson } from '~/contentScript/extractPersonProfile'; -import { isDefined } from '~/utils/isDefined'; // 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/ @@ -20,85 +19,5 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => { await insertButtonForPerson(); } - if (message.action === 'TOGGLE') { - await toggle(); - } - - if (message.action === 'AUTHENTICATED') { - await authenticated(); - } - sendResponse('Executing!'); }); - -const IFRAME_WIDTH = '400px'; - -const createIframe = () => { - const iframe = document.createElement('iframe'); - iframe.style.background = 'lightgrey'; - iframe.style.height = '100vh'; - iframe.style.width = IFRAME_WIDTH; - iframe.style.position = 'fixed'; - iframe.style.top = '0px'; - iframe.style.right = `-${IFRAME_WIDTH}`; - iframe.style.zIndex = '9000000000000000000'; - iframe.style.transition = 'ease-in-out 0.3s'; - return iframe; -}; - -const handleContentIframeLoadComplete = () => { - //If the pop-out window is already open then we replace loading iframe with our content iframe - if (optionsIframe.style.right === '0px') contentIframe.style.right = '0px'; - optionsIframe.style.display = 'none'; - contentIframe.style.display = 'block'; -}; - -//Creating one iframe where we are loading our front end in the background -const contentIframe = createIframe(); -contentIframe.style.display = 'none'; - -chrome.storage.local.get().then((store) => { - if (isDefined(store.loginToken)) { - contentIframe.src = `${import.meta.env.VITE_FRONT_BASE_URL}`; - contentIframe.onload = handleContentIframeLoadComplete; - } -}); - -const optionsIframe = createIframe(); -optionsIframe.src = chrome.runtime.getURL('options.html'); - -document.body.appendChild(contentIframe); -document.body.appendChild(optionsIframe); - -const toggleIframe = (iframe: HTMLIFrameElement) => { - if ( - iframe.style.right === `-${IFRAME_WIDTH}` && - iframe.style.display !== 'none' - ) { - iframe.style.right = '0px'; - } else if (iframe.style.right === '0px' && iframe.style.display !== 'none') { - iframe.style.right = `-${IFRAME_WIDTH}`; - } -}; - -const toggle = async () => { - const store = await chrome.storage.local.get(); - if (isDefined(store.accessToken)) { - toggleIframe(contentIframe); - } else { - toggleIframe(optionsIframe); - } -}; - -const authenticated = async () => { - const store = await chrome.storage.local.get(); - if (isDefined(store.loginToken)) { - contentIframe.src = `${ - import.meta.env.VITE_FRONT_BASE_URL - }/verify?loginToken=${store.loginToken.token}`; - contentIframe.onload = handleContentIframeLoadComplete; - toggleIframe(contentIframe); - } else { - toggleIframe(optionsIframe); - } -}; diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts b/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts new file mode 100644 index 000000000..9a21fc620 --- /dev/null +++ b/packages/twenty-chrome-extension/src/contentScript/utils/changeSidepanelUrl.ts @@ -0,0 +1,16 @@ +import { isDefined } from '~/utils/isDefined'; + +const changeSidePanelUrl = async (url: string) => { + const { tab: activeTab } = await chrome.runtime.sendMessage({ + action: 'getActiveTab', + }); + if (isDefined(activeTab) && isDefined(url)) { + chrome.storage.local.set({ [`sidepanelUrl_${activeTab.id}`]: url }); + chrome.runtime.sendMessage({ + action: 'changeSidepanelUrl', + message: { url }, + }); + } +}; + +export default changeSidePanelUrl; diff --git a/packages/twenty-chrome-extension/src/graphql/company/queries.ts b/packages/twenty-chrome-extension/src/graphql/company/queries.ts index b3fceedb2..7c546855e 100644 --- a/packages/twenty-chrome-extension/src/graphql/company/queries.ts +++ b/packages/twenty-chrome-extension/src/graphql/company/queries.ts @@ -5,6 +5,7 @@ export const FIND_COMPANY = gql` companies(filter: $filter) { edges { node { + id name linkedinLink { url diff --git a/packages/twenty-chrome-extension/src/graphql/person/queries.ts b/packages/twenty-chrome-extension/src/graphql/person/queries.ts index 83f402c1c..efbad9211 100644 --- a/packages/twenty-chrome-extension/src/graphql/person/queries.ts +++ b/packages/twenty-chrome-extension/src/graphql/person/queries.ts @@ -5,6 +5,7 @@ export const FIND_PERSON = gql` people(filter: $filter) { edges { node { + id name { firstName lastName diff --git a/packages/twenty-chrome-extension/src/manifest.ts b/packages/twenty-chrome-extension/src/manifest.ts index ed926ddd7..18f21d764 100644 --- a/packages/twenty-chrome-extension/src/manifest.ts +++ b/packages/twenty-chrome-extension/src/manifest.ts @@ -26,7 +26,7 @@ export default defineManifest({ action: {}, //TODO: change this to a documenation page - options_page: 'options.html', + options_page: 'sidepanel.html', background: { service_worker: 'src/background/index.ts', @@ -43,12 +43,12 @@ export default defineManifest({ web_accessible_resources: [ { - resources: ['options.html'], + resources: ['sidepanel.html', 'page-inaccessible.html'], matches: ['https://www.linkedin.com/*'], }, ], - permissions: ['activeTab', 'storage', 'identity'], + permissions: ['activeTab', 'storage', 'identity', 'sidePanel'], host_permissions: host_permissions, diff --git a/packages/twenty-chrome-extension/src/options/Options.tsx b/packages/twenty-chrome-extension/src/options/Options.tsx deleted file mode 100644 index 112caec5b..000000000 --- a/packages/twenty-chrome-extension/src/options/Options.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useEffect, useState } from 'react'; -import styled from '@emotion/styled'; - -import { Loader } from '@/ui/display/loader/components/Loader'; -import { MainButton } from '@/ui/input/button/MainButton'; -import { TextInput } from '@/ui/input/components/TextInput'; - -const StyledWrapper = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.noisy}; - display: flex; - height: 100vh; - justify-content: center; -`; - -const StyledContainer = styled.div` - background: ${({ theme }) => theme.background.primary}; - width: 400px; - height: 350px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledActionContainer = styled.div` - align-items: center; - display: flex; - flex-direction: column; - gap: 10px; - justify-content: center; - width: 300px; -`; - -const StyledLabel = styled.span` - color: ${({ theme }) => theme.font.color.primary}; - font-size: ${({ theme }) => theme.font.size.md}; - font-weight: ${({ theme }) => theme.font.weight.semiBold}; - margin-bottom: ${({ theme }) => theme.spacing(1)}; - text-transform: uppercase; -`; - -const Options = () => { - const [isAuthenticating, setIsAuthenticating] = useState(false); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [error, setError] = useState(''); - const [serverBaseUrl, setServerBaseUrl] = useState( - import.meta.env.VITE_SERVER_BASE_URL, - ); - const authenticate = () => { - setIsAuthenticating(true); - setError(''); - chrome.runtime.sendMessage({ action: 'CONNECT' }, ({ status, message }) => { - if (status === true) { - setIsAuthenticated(true); - setIsAuthenticating(false); - chrome.storage.local.set({ isAuthenticated: true }); - } else { - setError(message); - setIsAuthenticating(false); - } - }); - }; - - useEffect(() => { - const getState = async () => { - const store = await chrome.storage.local.get(); - if (store.serverBaseUrl !== '') { - setServerBaseUrl(store.serverBaseUrl); - } else { - setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL); - } - - if (store.isAuthenticated === true) setIsAuthenticated(true); - }; - void getState(); - }, []); - - const handleBaseUrlChange = (value: string) => { - setServerBaseUrl(value); - setError(''); - chrome.storage.local.set({ serverBaseUrl: value }); - }; - - return ( - - - twenty-logo - - - {isAuthenticating ? ( - - ) : isAuthenticated ? ( - Connected! - ) : ( - <> - authenticate()} - fullWidth - /> - window.open(`${serverBaseUrl}`, '_blank')} - fullWidth - /> - - )} - - - - ); -}; - -export default Options; diff --git a/packages/twenty-chrome-extension/src/options/PageInaccessible.tsx b/packages/twenty-chrome-extension/src/options/PageInaccessible.tsx new file mode 100644 index 000000000..3f0ade366 --- /dev/null +++ b/packages/twenty-chrome-extension/src/options/PageInaccessible.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; + +import { MainButton } from '@/ui/input/button/MainButton'; + +const StyledWrapper = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.primary}; + display: flex; + height: 100vh; + justify-content: center; +`; + +const StyledContainer = styled.div` + width: 400px; + height: 350px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledTextContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + justify-content: center; +`; + +const StyledLargeText = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.lg}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; +`; + +const StyledMediumText = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.md}; +`; + +const PageInaccessible = () => { + return ( + + + twenty-logo + + + Extension not available on the website + + + Open LinkedIn to use the extension + + + window.open('https://www.linkedin.com/')} + /> + + + ); +}; + +export default PageInaccessible; diff --git a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx new file mode 100644 index 000000000..674a3f799 --- /dev/null +++ b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx @@ -0,0 +1,169 @@ +import { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; + +import { Loader } from '@/ui/display/loader/components/Loader'; +import { MainButton } from '@/ui/input/button/MainButton'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { isDefined } from '~/utils/isDefined'; + +const StyledIframe = styled.iframe` + display: block; + width: 100%; + height: 100vh; + border: none; +`; + +const StyledWrapper = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.primary}; + display: flex; + height: 100vh; + justify-content: center; +`; + +const StyledContainer = styled.div` + width: 400px; + height: 350px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledActionContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + gap: 10px; + justify-content: center; + width: 300px; +`; + +const Sidepanel = () => { + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [iframeSrc, setIframeSrc] = useState( + import.meta.env.VITE_FRONT_BASE_URL, + ); + const [error, setError] = useState(''); + const [serverBaseUrl, setServerBaseUrl] = useState(''); + const authenticate = () => { + setIsAuthenticating(true); + setError(''); + chrome.runtime.sendMessage( + { action: 'launchOAuth' }, + ({ status, message }) => { + if (status === true) { + setIsAuthenticated(true); + setIsAuthenticating(false); + chrome.storage.local.set({ isAuthenticated: true }); + } else { + setError(message); + setIsAuthenticating(false); + } + }, + ); + }; + + useEffect(() => { + const getState = async () => { + const store = await chrome.storage.local.get(); + if (isDefined(store.serverBaseUrl)) { + setServerBaseUrl(store.serverBaseUrl); + } else { + setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL); + } + + if (store.isAuthenticated === true) setIsAuthenticated(true); + const { tab: activeTab } = await chrome.runtime.sendMessage({ + action: 'getActiveTab', + }); + + if ( + isDefined(activeTab) && + isDefined(store[`sidepanelUrl_${activeTab.id}`]) + ) { + const url = store[`sidepanelUrl_${activeTab.id}`]; + setIframeSrc(url); + } + }; + void getState(); + }, []); + + useEffect(() => { + const handleBrowserEvents = ({ action }: { action: string }) => { + if (action === 'changeSidepanelUrl') { + setIframeSrc(''); + } + }; + chrome.runtime.onMessage.addListener(handleBrowserEvents); + + return () => { + chrome.runtime.onMessage.removeListener(handleBrowserEvents); + }; + }, []); + + useEffect(() => { + const getIframeState = async () => { + const store = await chrome.storage.local.get(); + const { tab: activeTab } = await chrome.runtime.sendMessage({ + action: 'getActiveTab', + }); + + if ( + isDefined(activeTab) && + isDefined(store[`sidepanelUrl_${activeTab.id}`]) + ) { + const url = store[`sidepanelUrl_${activeTab.id}`]; + setIframeSrc(url); + } + }; + void getIframeState(); + }, [iframeSrc]); + + const handleBaseUrlChange = (value: string) => { + setServerBaseUrl(value); + setError(''); + chrome.storage.local.set({ serverBaseUrl: value }); + }; + + return isAuthenticated ? ( + + ) : ( + + + twenty-logo + {isAuthenticating ? ( + + ) : ( + + + authenticate()} + fullWidth + /> + + window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank') + } + fullWidth + /> + + )} + + + ); +}; + +export default Sidepanel; diff --git a/packages/twenty-chrome-extension/src/options/index.tsx b/packages/twenty-chrome-extension/src/options/index.tsx index e7920fc9e..1aef04520 100644 --- a/packages/twenty-chrome-extension/src/options/index.tsx +++ b/packages/twenty-chrome-extension/src/options/index.tsx @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { ThemeType } from '@/ui/theme/constants/ThemeLight'; -import Options from '~/options/Options'; +import Sidepanel from '~/options/Sidepanel'; import '~/index.css'; ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( - + , ); diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/input/button/MainButton.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/input/button/MainButton.tsx index 969ddf50e..b319b8070 100644 --- a/packages/twenty-chrome-extension/src/options/modules/ui/input/button/MainButton.tsx +++ b/packages/twenty-chrome-extension/src/options/modules/ui/input/button/MainButton.tsx @@ -22,7 +22,7 @@ const StyledButton = styled.button< switch (variant) { case 'primary': - return theme.background.radialGradient; + return theme.background.primaryInverted; case 'secondary': return theme.background.primary; default: @@ -37,7 +37,7 @@ const StyledButton = styled.button< switch (variant) { case 'primary': - return theme.background.transparent.light; + return theme.background.transparent.strong; case 'secondary': return theme.border.color.medium; default: @@ -59,7 +59,7 @@ const StyledButton = styled.button< switch (variant) { case 'primary': - return theme.grayScale.gray0; + return theme.font.color.inverted; case 'secondary': return theme.font.color.primary; default: @@ -88,7 +88,7 @@ const StyledButton = styled.button< default: return ` &:hover { - background: ${theme.background.radialGradientHover}}; + background: ${theme.background.primaryInvertedHover}}; } `; } diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts index 50c648c16..631039e72 100644 --- a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts +++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts @@ -23,4 +23,6 @@ export const BACKGROUND_DARK = { overlay: RGBA(GRAY_SCALE.gray80, 0.8), radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, + primaryInverted: GRAY_SCALE.gray20, + primaryInvertedHover: GRAY_SCALE.gray15, }; diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts index 9f4e6e4af..5df07a383 100644 --- a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts +++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts @@ -23,4 +23,6 @@ export const BACKGROUND_LIGHT = { overlay: RGBA(GRAY_SCALE.gray80, 0.8), radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, + primaryInverted: GRAY_SCALE.gray60, + primaryInvertedHover: GRAY_SCALE.gray55, }; diff --git a/packages/twenty-chrome-extension/src/options/page-inaccessible-index.tsx b/packages/twenty-chrome-extension/src/options/page-inaccessible-index.tsx new file mode 100644 index 000000000..1584913e2 --- /dev/null +++ b/packages/twenty-chrome-extension/src/options/page-inaccessible-index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; +import { ThemeType } from '@/ui/theme/constants/ThemeLight'; +import PageInaccessible from '~/options/PageInaccessible'; + +import '~/index.css'; + +ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( + + + + + , +); + +declare module '@emotion/react' { + export interface Theme extends ThemeType {} +} diff --git a/packages/twenty-chrome-extension/vite.config.ts b/packages/twenty-chrome-extension/vite.config.ts index f30851658..3302ac673 100644 --- a/packages/twenty-chrome-extension/vite.config.ts +++ b/packages/twenty-chrome-extension/vite.config.ts @@ -38,6 +38,8 @@ export default defineConfig(() => { hmr: { port: 3002 }, }, + cacheDir: './node_modules/.vite', + plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()], }; });