mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 12:22:29 +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. | ||||
| chrome.action.onClicked.addListener(() => { | ||||
|   openOptionsPage(); | ||||
| chrome.action.onClicked.addListener((tab) => { | ||||
|   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. | ||||
|   | ||||
| @@ -16,5 +16,55 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { | ||||
|     insertButtonForPerson(); | ||||
|   } | ||||
|  | ||||
|   if (message.action === 'TOGGLE') { | ||||
|     toggle(); | ||||
|   } | ||||
|  | ||||
|   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: {}, | ||||
|  | ||||
|   //TODO: change this to a documenation page | ||||
|   options_page: 'options.html', | ||||
|  | ||||
|   background: { | ||||
| @@ -34,4 +35,8 @@ export default defineManifest({ | ||||
|   permissions: ['activeTab', 'storage'], | ||||
|  | ||||
|   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 { ThemeType } from '@/ui/theme/constants/ThemeLight'; | ||||
| import App from '~/options/Options'; | ||||
| import Options from '~/options/Options'; | ||||
|  | ||||
| import '~/index.css'; | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ||||
|   <AppThemeProvider> | ||||
|     <React.StrictMode> | ||||
|       <App /> | ||||
|       <Options /> | ||||
|     </React.StrictMode> | ||||
|   </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; | ||||
| `; | ||||
|  | ||||
| const StyledImg = styled.img``; | ||||
| const StyledImgLogo = styled.img` | ||||
|   &:hover { | ||||
|     cursor: pointer; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| const StyledMain = styled.main` | ||||
|   margin-bottom: ${({ theme }) => theme.spacing(8)}; | ||||
| @@ -51,6 +55,13 @@ const StyledSection = styled.div<{ showSection: boolean }>` | ||||
|   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 = () => { | ||||
|   const [apiKey, setApiKey] = useState(''); | ||||
|   const [route, setRoute] = useState(''); | ||||
| @@ -73,10 +84,6 @@ export const ApiKeyForm = () => { | ||||
|     void getState(); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     chrome.storage.local.set({ apiKey }); | ||||
|   }, [apiKey]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (import.meta.env.VITE_SERVER_BASE_URL !== route) { | ||||
|       chrome.storage.local.set({ serverBaseUrl: route }); | ||||
| @@ -85,10 +92,18 @@ export const ApiKeyForm = () => { | ||||
|     } | ||||
|   }, [route]); | ||||
|  | ||||
|   const handleValidateKey = () => { | ||||
|     chrome.storage.local.set({ apiKey }); | ||||
|  | ||||
|     window.close(); | ||||
|   }; | ||||
|  | ||||
|   const handleGenerateClick = () => { | ||||
|     window.open( | ||||
|       `${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers/api-keys`, | ||||
|     ); | ||||
|     window.open(`${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers`); | ||||
|   }; | ||||
|  | ||||
|   const handleGoToTwenty = () => { | ||||
|     window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`); | ||||
|   }; | ||||
|  | ||||
|   const handleToggle = () => { | ||||
| @@ -98,9 +113,12 @@ export const ApiKeyForm = () => { | ||||
|   return ( | ||||
|     <StyledContainer isToggleOn={showSection}> | ||||
|       <StyledHeader> | ||||
|         <StyledImg src="/logo/32-32.svg" alt="Twenty Logo" /> | ||||
|         <StyledImgLogo | ||||
|           src="/logo/32-32.svg" | ||||
|           alt="Twenty Logo" | ||||
|           onClick={handleGoToTwenty} | ||||
|         /> | ||||
|       </StyledHeader> | ||||
|  | ||||
|       <StyledMain> | ||||
|         <H2Title | ||||
|           title="Connect your account" | ||||
| @@ -112,9 +130,10 @@ export const ApiKeyForm = () => { | ||||
|           onChange={setApiKey} | ||||
|           placeholder="My API key" | ||||
|         /> | ||||
|         <StyledButtonHorizontalContainer> | ||||
|           <Button | ||||
|             title="Generate a key" | ||||
|           fullWidth={false} | ||||
|             fullWidth={true} | ||||
|             variant="primary" | ||||
|             accent="default" | ||||
|             size="small" | ||||
| @@ -123,6 +142,18 @@ export const ApiKeyForm = () => { | ||||
|             disabled={false} | ||||
|             onClick={handleGenerateClick} | ||||
|           /> | ||||
|           <Button | ||||
|             title="Validate key" | ||||
|             fullWidth={true} | ||||
|             variant="primary" | ||||
|             accent="default" | ||||
|             size="small" | ||||
|             position="standalone" | ||||
|             soon={false} | ||||
|             disabled={apiKey === ''} | ||||
|             onClick={handleValidateKey} | ||||
|           /> | ||||
|         </StyledButtonHorizontalContainer> | ||||
|       </StyledMain> | ||||
|  | ||||
|       <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" | ||||
|     } | ||||
|   ] | ||||
|   ], | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Aditya Pimpalkar
					Aditya Pimpalkar