Skip to content

Commit

Permalink
Add button position configuration
Browse files Browse the repository at this point in the history
Allow users to control the position and attributed of the button on the page, including left/right, vertical, opacity, and size.
  • Loading branch information
paustint committed Jan 21, 2025
1 parent 8566752 commit 3003716
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 48 deletions.
10 changes: 0 additions & 10 deletions apps/jetstream-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,5 @@ export default defineConfig({
dependencies: ['setup'],
// teardown: 'teardown',
},

// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },

// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
],
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 28 additions & 28 deletions apps/jetstream-web-extension/src/components/SfdcPageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const ButtonLinkCss = css`

export function SfdcPageButton() {
const options = useRecoilValue(chromeStorageOptions);
const { authTokens } = useRecoilValue(chromeSyncStorage);
const { authTokens, buttonPosition } = useRecoilValue(chromeSyncStorage);
const [isOnSalesforcePage] = useState(
() => !!document.querySelector('body.sfdcBody, body.ApexCSIPage, #auraLoadingBox') || location.host.endsWith('visualforce.com')
);
Expand Down Expand Up @@ -202,33 +202,33 @@ export function SfdcPageButton() {
<button
data-testid="jetstream-ext-page-button"
css={css`
z-index: 1000;
display: block;
position: fixed;
vertical-align: middle;
pointer: cursor;
top: 210px;
right: ${isOpen ? '0px;' : '-5px;'};
opacity: ${isOpen ? '1;' : '0.25;'};
width: 25px;
transition: transform 0.3s ease;
transform: ${isOpen ? 'scale(1.5);' : 'scale(1);'}
transform-origin: top right;
z-index: 1000;
display: ${isOpen ? 'none' : 'block'};
position: fixed;
vertical-align: middle;
pointer: cursor;
top: clamp(1px, ${buttonPosition.position}px, 100vh);
${buttonPosition.location}: ${isOpen ? '0px' : '-5px'};
opacity: ${isOpen ? '1' : `${buttonPosition.opacity}`};
width: ${buttonPosition.inactiveSize}px;
transition: transform 0.3s ease;
transform: ${isOpen ? `scale(${buttonPosition.activeScale})` : 'scale(1)'};
transform-origin: top ${buttonPosition.location};
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
text-align: center;
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
text-align: center;
&:hover {
opacity: 1;
right: 0px;
transform: scale(1.5);
}
`}
&:hover {
opacity: 1;
${buttonPosition.location}: 0px;
transform: scale(${buttonPosition.activeScale});
}
`}
onClick={() => setIsOpen(true)}
>
<JetstreamIcon />
Expand All @@ -239,8 +239,8 @@ export function SfdcPageButton() {
z-index: 1000;
display: block;
position: fixed;
top: 160px;
right: 0;
top: clamp(1px, ${buttonPosition.position - 50}px, calc(100vh - 500px));
${buttonPosition.location}: 0;
width: 250px;
border-radius: var(--lwc-borderRadiusMedium, 0.25rem);
min-height: 2rem;
Expand Down
8 changes: 8 additions & 0 deletions apps/jetstream-web-extension/src/core/AppInitializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import localforage from 'localforage';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { chromeSyncStorage } from '../utils/extension.store';
import { sendMessage } from '../utils/web-extension.utils';
import { GlobalExtensionError } from './GlobalExtensionError';
import { GlobalExtensionLoggedOut } from './GlobalExtensionLoggedOut';

const args = new URLSearchParams(location.search.slice(1));
const salesforceHost = args.get('host');
Expand All @@ -29,6 +31,8 @@ export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onUserP
const location = useLocation();
const userProfile = useRecoilValue<UserProfileUi>(fromAppState.userProfileState);

const { authTokens } = useRecoilValue(chromeSyncStorage);

const setSelectedOrgId = useSetRecoilState(fromAppState.selectedOrgIdState);
const setSalesforceOrgs = useSetRecoilState(fromAppState.salesforceOrgsState);
const selectedOrg = useRecoilValue(fromAppState.selectedOrgState);
Expand Down Expand Up @@ -77,6 +81,10 @@ export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onUserP
return <GlobalExtensionError message={fatalError} />;
}

if (!authTokens?.loggedIn) {
return <GlobalExtensionLoggedOut />;
}

if (!salesforceHost || !selectedOrg?.uniqueId) {
return <AppLoading />;
}
Expand Down
37 changes: 37 additions & 0 deletions apps/jetstream-web-extension/src/core/GlobalExtensionLoggedOut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FeedbackLink, Grid } from '@jetstream/ui';
import { environment } from '../environments/environment';

export const GlobalExtensionLoggedOut = () => {
return (
<div className="slds-card slds-box">
<p>This page is only accessible when you are logged in to the Chrome extension. Login to continue.</p>
<a
href={`${environment.serverUrl}/web-extension/init`}
target="_blank"
className="slds-button slds-button_brand slds-m-top_small"
rel="noreferrer"
>
Sign In
</a>
<hr className="slds-m-vertical_medium" />
<Grid vertical>
<div>
<ol className="slds-list_ordered">
<li>
Bug reports and feature requests - <FeedbackLink type="GH_ISSUE" />
</li>
<li className="slds-m-top_x-small">
<FeedbackLink type="GH_DISCUSSION" />
</li>
<li className="slds-m-top_x-small">
Ask a question in the <strong>#vendors-jetstream</strong> Discord channel <FeedbackLink type="DISCORD" />
</li>
<li className="slds-m-top_x-small">
You can always email us at <FeedbackLink type="EMAIL" />
</li>
</ol>
</div>
</Grid>
</div>
);
};
1 change: 1 addition & 0 deletions apps/jetstream-web-extension/src/pages/app/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="assets/icons/jetstream-icon-inverse-16.png" type="image/png" />
<title>Jetstream</title>
</head>

Expand Down
126 changes: 122 additions & 4 deletions apps/jetstream-web-extension/src/pages/popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useNonInitialEffect } from '@jetstream/shared/ui-utils';
import { CheckboxToggle, FeedbackLink, ScopedNotification } from '@jetstream/ui';
import { useEffect, useState } from 'react';
import { css } from '@emotion/react';
import { useDebounce, useNonInitialEffect } from '@jetstream/shared/ui-utils';
import { CheckboxToggle, FeedbackLink, RadioButton, RadioGroup, ScopedNotification, Slider } from '@jetstream/ui';
import { useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import JetstreamIcon from '../../components/icons/JetstreamIcon';
import JetstreamLogo from '../../components/icons/JetstreamLogo';
import { AppWrapperNotJetstreamOwnedPage } from '../../core/AppWrapperNotJetstreamOwnedPage';
import { environment } from '../../environments/environment';
import { chromeStorageOptions, chromeSyncStorage } from '../../utils/extension.store';
import { ButtonPosition, DEFAULT_BUTTON_POSITION } from '../../utils/extension.types';
import { initAndRenderReact, sendMessage } from '../../utils/web-extension.utils';

initAndRenderReact(
Expand All @@ -18,10 +21,37 @@ export function Component() {
const options = useRecoilValue(chromeStorageOptions);
const [enabled, setEnabled] = useState(options.enabled);
const [authError, setAuthError] = useState<string | null>(null);
const { authTokens } = useRecoilValue(chromeSyncStorage);
const { authTokens, buttonPosition: _buttonPosition } = useRecoilValue(chromeSyncStorage);

const [buttonOptionsVisible, setButtonOptionsVisible] = useState(false);

const [buttonLocation, setButtonLocation] = useState(_buttonPosition.location);
const [buttonPosition, setButtonPosition] = useState(_buttonPosition.position);
const [buttonOpacity, setButtonOpacity] = useState(_buttonPosition.opacity);
const [buttonInactiveSize, setButtonInactiveSize] = useState(_buttonPosition.inactiveSize);
const [buttonActiveScale, setButtonActiveScale] = useState(_buttonPosition.activeScale);

const currentButtonPosition = useMemo(
(): ButtonPosition => ({
location: buttonLocation,
position: buttonPosition,
opacity: buttonOpacity,
inactiveSize: buttonInactiveSize,
activeScale: buttonActiveScale,
}),
[buttonActiveScale, buttonInactiveSize, buttonLocation, buttonOpacity, buttonPosition]
);

const currentButtonPositionDebounced = useDebounce(currentButtonPosition, 500);

const loggedIn = !!authTokens?.loggedIn;

useNonInitialEffect(() => {
chrome.storage.sync.set({ buttonPosition: currentButtonPositionDebounced }).catch((ex) => {
console.warn('Error setting button position', ex);
});
}, [currentButtonPositionDebounced]);

useEffect(() => {
sendMessage({ message: 'VERIFY_AUTH' }).catch((err) => {
setAuthError('There was a problem verifying your authentication. Please log in again.');
Expand All @@ -44,6 +74,15 @@ export function Component() {
});
}

function handleResetButtonOptions() {
setButtonLocation(DEFAULT_BUTTON_POSITION.location);
setButtonPosition(DEFAULT_BUTTON_POSITION.position);
setButtonOpacity(DEFAULT_BUTTON_POSITION.opacity);
setButtonInactiveSize(DEFAULT_BUTTON_POSITION.inactiveSize);
setButtonActiveScale(DEFAULT_BUTTON_POSITION.activeScale);
setButtonOptionsVisible(false);
}

return (
<>
<header className="slds-m-bottom_medium">
Expand Down Expand Up @@ -71,6 +110,13 @@ export function Component() {
/>
<hr className="slds-m-vertical_small" />
<p>Visit any Salesforce page to see the floating Jetstream button icon on the right side of the page.</p>
<div
css={css`
width: 25px;
`}
>
<JetstreamIcon />
</div>
<hr className="slds-m-vertical_small" />
<p className="slds-m-bottom_x-small">Feedback or Suggestions?</p>
<div>
Expand All @@ -79,6 +125,78 @@ export function Component() {
<div>
<FeedbackLink type="EMAIL" label="Send us an email" />
</div>
<hr className="slds-m-vertical_small" />
<p>Salesforce Button Location</p>
{!buttonOptionsVisible && (
<button className="slds-button slds-m-top_x-small" onClick={() => setButtonOptionsVisible(true)}>
Show Configuration Options
</button>
)}
{buttonOptionsVisible && (
<>
<button className="slds-button slds-m-top_x-small slds-m-bottom_small" onClick={() => handleResetButtonOptions()}>
Reset to Default
</button>
<RadioGroup label="Name Type" isButtonGroup className="slds-m-bottom_small">
<RadioButton
id="sfdc-button-slider-location-left"
name="sfdc-button-slider-location"
label="Left"
value="left"
checked={buttonLocation === 'left'}
onChange={(value) => setButtonLocation(value as 'left')}
/>
<RadioButton
id="sfdc-button-slider-location-rigt"
name="sfdc-button-slider-location"
label="Right"
value="right"
checked={buttonLocation === 'right'}
onChange={(value) => setButtonLocation(value as 'right')}
/>
</RadioGroup>
<Slider
id="sfdc-button-slider-position"
value={`${buttonPosition}`}
label="Vertical Position"
labelHelp="The vertical position of the Jetstream icon on the page."
min={1}
max={window.screen.height - 50 || 1000}
step={10}
onChange={(value) => setButtonPosition(parseInt(value, 10))}
/>
<Slider
id="sfdc-button-slider-opacity"
value={`${buttonOpacity}`}
label="Jetstream Icon Opacity"
labelHelp="Opacity of the Jetstream icon when it is not being hovered over."
min={0.1}
max={1}
step={0.05}
onChange={(value) => setButtonOpacity(Number(value))}
/>
<Slider
id="sfdc-button-slider-inactive-size"
value={`${buttonInactiveSize}`}
label="Jetstream Icon Size"
labelHelp="Size of the Jetstream icon when it is not being hovered over."
min={20}
max={100}
step={5}
onChange={(value) => setButtonInactiveSize(Number(value))}
/>
<Slider
id="sfdc-button-slider-active-scale"
value={`${buttonActiveScale}`}
label="Button Hover Scale"
labelHelp="The scale of the button when you hover over it. 1 is the same size as the non-hovered button."
min={1}
max={5}
step={0.1}
onChange={(value) => setButtonActiveScale(Number(value))}
/>
</>
)}
</>
)}
{(!loggedIn || !authTokens) && (
Expand Down
2 changes: 1 addition & 1 deletion apps/jetstream-web-extension/src/pages/popup/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<style>
body {
width: 350px;
height: 400px;
min-height: 400px;
background-color: white !important;
padding: 15px;
}
Expand Down
9 changes: 7 additions & 2 deletions apps/jetstream-web-extension/src/utils/extension.store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { atom, selector } from 'recoil';
import { setRecoil } from 'recoil-nexus';
import { ChromeStorageState } from './extension.types';
import { ChromeStorageState, DEFAULT_BUTTON_POSITION } from './extension.types';

chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' || namespace === 'sync') {
Expand All @@ -23,6 +23,7 @@ chrome.storage.onChanged.addListener((changes, namespace) => {

newState.sync.authTokens = newState.sync.authTokens ?? null;
newState.sync.extIdentifier = newState.sync.extIdentifier ?? null;
newState.sync.buttonPosition = newState.sync.buttonPosition ?? DEFAULT_BUTTON_POSITION;

return newState;
});
Expand All @@ -32,7 +33,7 @@ chrome.storage.onChanged.addListener((changes, namespace) => {
async function initAuthState(): Promise<ChromeStorageState> {
const [local, sync] = await Promise.all([
chrome.storage.local.get(['options', 'connections']),
chrome.storage.sync.get(['extIdentifier', 'authTokens']),
chrome.storage.sync.get(['extIdentifier', 'authTokens', 'buttonPosition']),
]);
return {
local: {
Expand All @@ -46,6 +47,10 @@ async function initAuthState(): Promise<ChromeStorageState> {
...(sync as ChromeStorageState['sync']),
authTokens: (sync as ChromeStorageState['sync'])?.authTokens ?? null,
extIdentifier: (sync as ChromeStorageState['sync'])?.extIdentifier ?? null,
buttonPosition: {
...DEFAULT_BUTTON_POSITION,
...(sync as ChromeStorageState['sync'])?.buttonPosition,
},
},
};
}
Expand Down
Loading

0 comments on commit 3003716

Please sign in to comment.