Skip to content

Commit

Permalink
Merge pull request #1170 from jetstreamapp/feat/add-search-to-web-ext…
Browse files Browse the repository at this point in the history
…ension

Web Extension: add user search and record search
  • Loading branch information
paustint authored Feb 8, 2025
2 parents c471b8c + cdd49bd commit 7890cb4
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 129 deletions.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export function PopupLogin() {
export function PagePopupDisplay() {
return (
<div className={styles.container}>
<img src={require('./chrome-extension-page-button.png').default} alt="Chrome Extension page button" />
<img src={require('./chrome-extension-page-menu-no-record.png').default} alt="Chrome Extension page popup" />
<img src={require('./chrome-extension-page-menu-record.png').default} alt="Chrome Extension page record page popup" />
<img src={require('./chrome-extension-page-menu-user-search.png').default} alt="Chrome Extension user search" />
</div>
);
}
Expand Down Expand Up @@ -84,6 +84,18 @@ Once you are logged in, the chrome extension will indicate that you are logged i

Visit any Salesforce page and you will see a Jetstream Icon conveniently located in the right side of the page. Click on the icon to open the Jetstream Chrome Extension.

<img src={require('./chrome-extension-page-button.png').default} alt="Chrome Extension page button" />

### Using the Page Popup

Open the Jetstream Chrome Extension on any Salesforce page to quickly access the following features:

- Quick access to common features, such as: Query, Load, Automation Control, and Permission Manager
- View or edit the currently open record _(appears when you visit a record page)_
- Paste in a record id to view or edit any record in Jetstream
- Search for any user in your org with the option to open in Salesforce or Jetstream directly
- You can also login as the user if you have permission to do so

<PagePopupDisplay />

### Options
Expand Down
230 changes: 104 additions & 126 deletions apps/jetstream-web-extension/src/components/SfdcPageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,19 @@
import { css } from '@emotion/react';
import { logger } from '@jetstream/shared/client-logger';
import { APP_ROUTES } from '@jetstream/shared/ui-router';
import { useInterval } from '@jetstream/shared/ui-utils';
import type { Maybe } from '@jetstream/types';
import { Grid, GridCol, OutsideClickHandler } from '@jetstream/ui';
import { Grid, GridCol, OutsideClickHandler, Tabs } from '@jetstream/ui';
import { fromAppState } from '@jetstream/ui/app-state';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import '../sfdc-styles-shim.scss';
import { chromeStorageOptions, chromeSyncStorage } from '../utils/extension.store';
import { getRecordPageRecordId, sendMessage } from '../utils/web-extension.utils';
import JetstreamIcon from './icons/JetstreamIcon';
import JetstreamLogo from './icons/JetstreamLogo';

function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>();

useEffect(() => {
savedCallback.current = callback;
}, [callback]);

useEffect(() => {
function tick() {
if (savedCallback.current) {
savedCallback.current();
}
}

if (delay !== null && delay !== undefined) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
import { SfdcPageButtonRecordSearch } from './SfdcPageButtonRecordSearch';
import { SfdcPageButtonUserSearch } from './SfdcPageButtonUserSearch';

const PAGE_LINKS = [
{
Expand Down Expand Up @@ -124,6 +106,33 @@ const ButtonLinkCss = css`
cursor: pointer;
`;

const ItemColStyles = css`
padding-right: var(--lwc-spacingSmall, 0.75rem);
padding-left: var(--lwc-spacingSmall, 0.75rem);
margin-bottom: var(--lwc-spacingXSmall, 0.5rem);
flex: 1 1 auto;
`;

const HorizontalRule = () => {
return (
<hr
className="slds-m-vertical_medium"
css={css`
margin-top: 0.5rem;
margin-bottom: 1rem;
display: block;
border: 0;
border-top: 1px solid rgb(229, 229, 229);
height: 1px;
clear: both;
padding: 0;
box-sizing: content-box;
width: 100%;
`}
/>
);
};

export function SfdcPageButton() {
const options = useRecoilValue(chromeStorageOptions);
const { authTokens, buttonPosition } = useRecoilValue(chromeSyncStorage);
Expand Down Expand Up @@ -241,7 +250,7 @@ export function SfdcPageButton() {
position: fixed;
top: clamp(1px, ${buttonPosition.position - 50}px, calc(100vh - 500px));
${buttonPosition.location}: 0;
width: 250px;
width: 300px;
border-radius: var(--lwc-borderRadiusMedium, 0.25rem);
min-height: 2rem;
background-color: var(--slds-g-color-neutral-base-100, var(--lwc-colorBackgroundAlt, rgb(255, 255, 255)));
Expand Down Expand Up @@ -277,114 +286,83 @@ export function SfdcPageButton() {
</header>
<div
data-testid="jetstream-ext-popup-body"
className="slds-popover__body slds-p-around_small slds-is-relative"
className="slds-popover__body slds-is-relative"
css={css`
position: relative;
padding: var(--lwc-spacingSmall, 0.75rem);
word-wrap: break-word;
`}
>
<Grid
vertical
gutters
css={css`
margin-right: calc(-1 * var(--lwc-spacingSmall, 0.75rem));
margin-left: calc(-1 * var(--lwc-spacingSmall, 0.75rem));
flex-direction: column;
display: flex;
`}
>
{PAGE_LINKS.map((item) => (
<GridCol
key={item.link}
className="slds-m-bottom_x-small"
css={css`
padding-right: var(--lwc-spacingSmall, 0.75rem);
padding-left: var(--lwc-spacingSmall, 0.75rem);
margin-bottom: var(--lwc-spacingXSmall, 0.5rem);
flex: 1 1 auto;
`}
>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&url=${encodeURIComponent(item.link)}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
>
{item.label}
</a>
</GridCol>
))}
{recordId && (
<>
<hr
className="slds-m-vertical_medium"
css={css`
margin-top: 0.5rem;
margin-bottom: 1rem;
display: block;
border: 0;
border-top: 1px solid rgb(229, 229, 229);
height: 1px;
clear: both;
padding: 0;
box-sizing: content-box;
width: 100%;
`}
/>
<GridCol
className="slds-m-bottom_x-small"
css={css`
padding-right: var(--lwc-spacingSmall, 0.75rem);
padding-left: var(--lwc-spacingSmall, 0.75rem);
margin-bottom: var(--lwc-spacingXSmall, 0.5rem);
flex: 1 1 auto;
`}
>
<p className="slds-text-heading_small slds-text-align_center slds-m-bottom_small">Record Actions</p>
</GridCol>
<GridCol
className="slds-m-bottom_x-small"
css={css`
padding-right: var(--lwc-spacingSmall, 0.75rem);
padding-left: var(--lwc-spacingSmall, 0.75rem);
margin-bottom: var(--lwc-spacingXSmall, 0.5rem);
flex: 1 1 auto;
`}
>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=VIEW_RECORD&actionValue=${recordId}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
>
View Current Record
</a>
</GridCol>
<GridCol
className="slds-m-bottom_x-small"
css={css`
padding-right: var(--lwc-spacingSmall, 0.75rem);
padding-left: var(--lwc-spacingSmall, 0.75rem);
margin-bottom: var(--lwc-spacingXSmall, 0.5rem);
flex: 1 1 auto;
`}
>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=EDIT_RECORD&actionValue=${recordId}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
<Tabs
tabs={[
{
id: 'actions',
title: 'Actions',
content: (
<Grid
vertical
gutters
css={css`
margin-right: calc(-1 * var(--lwc-spacingSmall, 0.75rem));
margin-left: calc(-1 * var(--lwc-spacingSmall, 0.75rem));
flex-direction: column;
display: flex;
`}
>
Edit Current Record
</a>
</GridCol>
</>
)}
</Grid>
{PAGE_LINKS.map((item) => (
<GridCol key={item.link} className="slds-m-bottom_x-small" css={ItemColStyles}>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&url=${encodeURIComponent(item.link)}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
>
{item.label}
</a>
</GridCol>
))}

<HorizontalRule />

{recordId && (
<>
<GridCol className="slds-m-bottom_x-small" css={ItemColStyles}>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=VIEW_RECORD&actionValue=${recordId}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
>
View Current Record
</a>
</GridCol>
<GridCol className="slds-m-bottom_x-small" css={ItemColStyles}>
<a
href={`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=EDIT_RECORD&actionValue=${recordId}`}
className="slds-button slds-button_neutral slds-button_stretch"
target="_blank"
rel="noreferrer"
css={ButtonLinkCss}
>
Edit Current Record
</a>
</GridCol>
</>
)}
<GridCol className="slds-m-bottom_x-small" css={ItemColStyles}>
<SfdcPageButtonRecordSearch sfHost={sfHost} />
</GridCol>
</Grid>
),
},
{
id: 'user-search',
title: 'User Search',
content: <SfdcPageButtonUserSearch sfHost={sfHost} />,
},
]}
/>
</div>
</OutsideClickHandler>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Grid, Input } from '@jetstream/ui';
import { useState } from 'react';
import '../sfdc-styles-shim.scss';

interface SfdcPageButtonRecordSearchProps {
sfHost: string;
}

export function SfdcPageButtonRecordSearch({ sfHost }: SfdcPageButtonRecordSearchProps) {
const [recordId, setRecordId] = useState('');

const isValidRecordId = recordId.length === 15 || recordId.length === 18;

function handleSubmit(ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault();
if (!isValidRecordId) {
return;
}
window.open(`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=VIEW_RECORD&actionValue=${recordId}`, '_blank');
}

function handleEditRecord() {
if (!isValidRecordId) {
return;
}
window.open(`${chrome.runtime.getURL('app.html')}?host=${sfHost}&action=EDIT_RECORD&actionValue=${recordId}`, '_blank');
}

return (
<form onSubmit={handleSubmit}>
<Input label="Open record in Jetstream">
<input
className="slds-input"
autoFocus
placeholder="Enter a record id"
minLength={15}
maxLength={18}
value={recordId}
onChange={(event) => setRecordId(event.target.value)}
/>
</Input>
<Grid className="slds-m-top_x-small">
<button className="slds-button slds-button_stretch slds-button_brand" type="submit" disabled={!isValidRecordId}>
View Record
</button>
<button
className="slds-button slds-button_stretch slds-button_neutral"
type="button"
disabled={!isValidRecordId}
onClick={() => handleEditRecord()}
>
Edit Record
</button>
</Grid>
</form>
);
}
Loading

0 comments on commit 7890cb4

Please sign in to comment.