mirror of
https://github.com/lingble/twenty.git
synced 2025-10-30 04:12:28 +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,17 +130,30 @@ export const ApiKeyForm = () => {
|
||||
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}
|
||||
/>
|
||||
<StyledButtonHorizontalContainer>
|
||||
<Button
|
||||
title="Generate a key"
|
||||
fullWidth={true}
|
||||
variant="primary"
|
||||
accent="default"
|
||||
size="small"
|
||||
position="standalone"
|
||||
soon={false}
|
||||
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