mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	feat: iframe addition (chrome-extension) (#4418)
* toggle iframe addition * React UI init * remove files * loading state files * render iframe logic * remove event * build fix * WIP * Ok * Cleaned --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
		
							
								
								
									
										12
									
								
								packages/twenty-chrome-extension/loading.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/twenty-chrome-extension/loading.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/loading-index.tsx"></script> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										
											BIN
										
									
								
								packages/twenty-chrome-extension/public/light-noise.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/twenty-chrome-extension/public/light-noise.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 9.4 KiB | 
| @@ -8,8 +8,8 @@ chrome.runtime.onInstalled.addListener((details) => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| // Open options page when extension icon is clicked. | // Open options page when extension icon is clicked. | ||||||
| chrome.action.onClicked.addListener(() => { | chrome.action.onClicked.addListener((tab) => { | ||||||
|   openOptionsPage(); |   chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // This listens for an event from other parts of the extension, such as the content script, and performs the required tasks. | // This listens for an event from other parts of the extension, such as the content script, and performs the required tasks. | ||||||
|   | |||||||
| @@ -16,5 +16,55 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { | |||||||
|     insertButtonForPerson(); |     insertButtonForPerson(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (message.action === 'TOGGLE') { | ||||||
|  |     toggle(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   sendResponse('Executing!'); |   sendResponse('Executing!'); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | const createIframe = () => { | ||||||
|  |   const iframe = document.createElement('iframe'); | ||||||
|  |   iframe.style.background = 'lightgrey'; | ||||||
|  |   iframe.style.height = '100vh'; | ||||||
|  |   iframe.style.width = '400px'; | ||||||
|  |   iframe.style.position = 'fixed'; | ||||||
|  |   iframe.style.top = '0px'; | ||||||
|  |   iframe.style.right = '-400px'; | ||||||
|  |   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 (loadingIframe.style.right === '0px') contentIframe.style.right = '0px'; | ||||||
|  |   loadingIframe.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'; | ||||||
|  | contentIframe.src = `${import.meta.env.VITE_FRONT_BASE_URL}`; | ||||||
|  | contentIframe.onload = handleContentIframeLoadComplete; | ||||||
|  |  | ||||||
|  | //Creating this iframe to show as a loading state until the above iframe loads completely | ||||||
|  | const loadingIframe = createIframe(); | ||||||
|  | loadingIframe.src = chrome.runtime.getURL('loading.html'); | ||||||
|  |  | ||||||
|  | document.body.appendChild(loadingIframe); | ||||||
|  | document.body.appendChild(contentIframe); | ||||||
|  |  | ||||||
|  | const toggleIframe = (iframe: HTMLIFrameElement) => { | ||||||
|  |   if (iframe.style.right === '-400px' && iframe.style.display !== 'none') { | ||||||
|  |     iframe.style.right = '0px'; | ||||||
|  |   } else if (iframe.style.right === '0px' && iframe.style.display !== 'none') { | ||||||
|  |     iframe.style.right = '-400px'; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const toggle = () => { | ||||||
|  |   toggleIframe(loadingIframe); | ||||||
|  |   toggleIframe(contentIframe); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ export default defineManifest({ | |||||||
|  |  | ||||||
|   action: {}, |   action: {}, | ||||||
|  |  | ||||||
|  |   //TODO: change this to a documenation page | ||||||
|   options_page: 'options.html', |   options_page: 'options.html', | ||||||
|  |  | ||||||
|   background: { |   background: { | ||||||
| @@ -34,4 +35,8 @@ export default defineManifest({ | |||||||
|   permissions: ['activeTab', 'storage'], |   permissions: ['activeTab', 'storage'], | ||||||
|  |  | ||||||
|   host_permissions: ['https://www.linkedin.com/*'], |   host_permissions: ['https://www.linkedin.com/*'], | ||||||
|  |  | ||||||
|  |   externally_connectable: { | ||||||
|  |     matches: [`https://app.twenty.com/*`, `http://localhost:3001/*`], | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								packages/twenty-chrome-extension/src/options/Loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/twenty-chrome-extension/src/options/Loading.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | import styled from '@emotion/styled'; | ||||||
|  |  | ||||||
|  | import { Loader } from '@/ui/display/loader/components/Loader'; | ||||||
|  |  | ||||||
|  | const StyledContainer = styled.div` | ||||||
|  |   align-items: center; | ||||||
|  |   background: ${({ theme }) => theme.background.noisy}; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   height: 100vh; | ||||||
|  |   gap: ${({ theme }) => theme.spacing(2)}; | ||||||
|  |   justify-content: center; | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const Loading = () => { | ||||||
|  |   return ( | ||||||
|  |     <StyledContainer> | ||||||
|  |       <img src="/logo/32-32.svg" alt="twenty-logo" height={64} width={64} /> | ||||||
|  |       <Loader /> | ||||||
|  |     </StyledContainer> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Loading; | ||||||
| @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client'; | |||||||
|  |  | ||||||
| import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; | import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; | ||||||
| import { ThemeType } from '@/ui/theme/constants/ThemeLight'; | import { ThemeType } from '@/ui/theme/constants/ThemeLight'; | ||||||
| import App from '~/options/Options'; | import Options from '~/options/Options'; | ||||||
|  |  | ||||||
| import '~/index.css'; | import '~/index.css'; | ||||||
|  |  | ||||||
| ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ||||||
|   <AppThemeProvider> |   <AppThemeProvider> | ||||||
|     <React.StrictMode> |     <React.StrictMode> | ||||||
|       <App /> |       <Options /> | ||||||
|     </React.StrictMode> |     </React.StrictMode> | ||||||
|   </AppThemeProvider>, |   </AppThemeProvider>, | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -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 Loading from '~/options/Loading'; | ||||||
|  |  | ||||||
|  | import '~/index.css'; | ||||||
|  |  | ||||||
|  | ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ||||||
|  |   <AppThemeProvider> | ||||||
|  |     <React.StrictMode> | ||||||
|  |       <Loading /> | ||||||
|  |     </React.StrictMode> | ||||||
|  |   </AppThemeProvider>, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | declare module '@emotion/react' { | ||||||
|  |   export interface Theme extends ThemeType {} | ||||||
|  | } | ||||||
| @@ -23,7 +23,11 @@ const StyledHeader = styled.header` | |||||||
|   text-align: center; |   text-align: center; | ||||||
| `; | `; | ||||||
|  |  | ||||||
| const StyledImg = styled.img``; | const StyledImgLogo = styled.img` | ||||||
|  |   &:hover { | ||||||
|  |     cursor: pointer; | ||||||
|  |   } | ||||||
|  | `; | ||||||
|  |  | ||||||
| const StyledMain = styled.main` | const StyledMain = styled.main` | ||||||
|   margin-bottom: ${({ theme }) => theme.spacing(8)}; |   margin-bottom: ${({ theme }) => theme.spacing(8)}; | ||||||
| @@ -51,6 +55,13 @@ const StyledSection = styled.div<{ showSection: boolean }>` | |||||||
|   max-height: ${({ showSection }) => (showSection ? '200px' : '0')}; |   max-height: ${({ showSection }) => (showSection ? '200px' : '0')}; | ||||||
| `; | `; | ||||||
|  |  | ||||||
|  | const StyledButtonHorizontalContainer = styled.div` | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   gap: ${({ theme }) => theme.spacing(4)}; | ||||||
|  |   width: 100%; | ||||||
|  | `; | ||||||
|  |  | ||||||
| export const ApiKeyForm = () => { | export const ApiKeyForm = () => { | ||||||
|   const [apiKey, setApiKey] = useState(''); |   const [apiKey, setApiKey] = useState(''); | ||||||
|   const [route, setRoute] = useState(''); |   const [route, setRoute] = useState(''); | ||||||
| @@ -73,10 +84,6 @@ export const ApiKeyForm = () => { | |||||||
|     void getState(); |     void getState(); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     chrome.storage.local.set({ apiKey }); |  | ||||||
|   }, [apiKey]); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (import.meta.env.VITE_SERVER_BASE_URL !== route) { |     if (import.meta.env.VITE_SERVER_BASE_URL !== route) { | ||||||
|       chrome.storage.local.set({ serverBaseUrl: route }); |       chrome.storage.local.set({ serverBaseUrl: route }); | ||||||
| @@ -85,10 +92,18 @@ export const ApiKeyForm = () => { | |||||||
|     } |     } | ||||||
|   }, [route]); |   }, [route]); | ||||||
|  |  | ||||||
|  |   const handleValidateKey = () => { | ||||||
|  |     chrome.storage.local.set({ apiKey }); | ||||||
|  |  | ||||||
|  |     window.close(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const handleGenerateClick = () => { |   const handleGenerateClick = () => { | ||||||
|     window.open( |     window.open(`${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers`); | ||||||
|       `${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers/api-keys`, |   }; | ||||||
|     ); |  | ||||||
|  |   const handleGoToTwenty = () => { | ||||||
|  |     window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleToggle = () => { |   const handleToggle = () => { | ||||||
| @@ -98,9 +113,12 @@ export const ApiKeyForm = () => { | |||||||
|   return ( |   return ( | ||||||
|     <StyledContainer isToggleOn={showSection}> |     <StyledContainer isToggleOn={showSection}> | ||||||
|       <StyledHeader> |       <StyledHeader> | ||||||
|         <StyledImg src="/logo/32-32.svg" alt="Twenty Logo" /> |         <StyledImgLogo | ||||||
|  |           src="/logo/32-32.svg" | ||||||
|  |           alt="Twenty Logo" | ||||||
|  |           onClick={handleGoToTwenty} | ||||||
|  |         /> | ||||||
|       </StyledHeader> |       </StyledHeader> | ||||||
|  |  | ||||||
|       <StyledMain> |       <StyledMain> | ||||||
|         <H2Title |         <H2Title | ||||||
|           title="Connect your account" |           title="Connect your account" | ||||||
| @@ -112,9 +130,10 @@ export const ApiKeyForm = () => { | |||||||
|           onChange={setApiKey} |           onChange={setApiKey} | ||||||
|           placeholder="My API key" |           placeholder="My API key" | ||||||
|         /> |         /> | ||||||
|  |         <StyledButtonHorizontalContainer> | ||||||
|           <Button |           <Button | ||||||
|             title="Generate a key" |             title="Generate a key" | ||||||
|           fullWidth={false} |             fullWidth={true} | ||||||
|             variant="primary" |             variant="primary" | ||||||
|             accent="default" |             accent="default" | ||||||
|             size="small" |             size="small" | ||||||
| @@ -123,6 +142,18 @@ export const ApiKeyForm = () => { | |||||||
|             disabled={false} |             disabled={false} | ||||||
|             onClick={handleGenerateClick} |             onClick={handleGenerateClick} | ||||||
|           /> |           /> | ||||||
|  |           <Button | ||||||
|  |             title="Validate key" | ||||||
|  |             fullWidth={true} | ||||||
|  |             variant="primary" | ||||||
|  |             accent="default" | ||||||
|  |             size="small" | ||||||
|  |             position="standalone" | ||||||
|  |             soon={false} | ||||||
|  |             disabled={apiKey === ''} | ||||||
|  |             onClick={handleValidateKey} | ||||||
|  |           /> | ||||||
|  |         </StyledButtonHorizontalContainer> | ||||||
|       </StyledMain> |       </StyledMain> | ||||||
|  |  | ||||||
|       <StyledFooter> |       <StyledFooter> | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | import styled from '@emotion/styled'; | ||||||
|  | import { motion } from 'framer-motion'; | ||||||
|  |  | ||||||
|  | const StyledLoaderContainer = styled.div` | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   display: flex; | ||||||
|  |   gap: ${({ theme }) => theme.spacing(2)}; | ||||||
|  |   width: ${({ theme }) => theme.spacing(6)}; | ||||||
|  |   height: ${({ theme }) => theme.spacing(3)}; | ||||||
|  |   border-radius: ${({ theme }) => theme.border.radius.pill}; | ||||||
|  |   border: 1px solid ${({ theme }) => theme.font.color.tertiary}; | ||||||
|  |   overflow: hidden; | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const StyledLoader = styled(motion.div)` | ||||||
|  |   background-color: ${({ theme }) => theme.font.color.tertiary}; | ||||||
|  |   border-radius: ${({ theme }) => theme.border.radius.pill}; | ||||||
|  |   height: 8px; | ||||||
|  |   width: 8px; | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | export const Loader = () => ( | ||||||
|  |   <StyledLoaderContainer> | ||||||
|  |     <StyledLoader | ||||||
|  |       animate={{ | ||||||
|  |         x: [-16, 0, 16], | ||||||
|  |         width: [8, 12, 8], | ||||||
|  |         height: [8, 2, 8], | ||||||
|  |       }} | ||||||
|  |       transition={{ | ||||||
|  |         duration: 0.8, | ||||||
|  |         times: [0, 0.15, 0.3], | ||||||
|  |         repeat: Infinity, | ||||||
|  |       }} | ||||||
|  |     /> | ||||||
|  |   </StyledLoaderContainer> | ||||||
|  | ); | ||||||
| @@ -39,5 +39,5 @@ | |||||||
|     { |     { | ||||||
|       "path": "./tsconfig.spec.json" |       "path": "./tsconfig.spec.json" | ||||||
|     } |     } | ||||||
|   ] |   ], | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Aditya Pimpalkar
					Aditya Pimpalkar