mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Update HelpScout beacon to work with embedded documentation articles.
Summary: - Fix base href in HelpScout beacon when showing articles (in particular for Firefox) - Show the 'Answers' tab normally except when reporting an error. - Combine the "Give Feedback" and "Help Center" buttons into one that normally opens the beacon (with a link to Help Center and to Community Forum), and a smaller one that opens the Help Center site in a new tab. - Update HELP_SCOUT_* env vars to use _V2 suffix, to allow them to coexist with code using the previous beacon. Test Plan: Updated the browser test to check the new behavior. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3170
This commit is contained in:
parent
6b448567c9
commit
8100272e9a
@ -29,6 +29,8 @@ export type BeaconCmd = 'init' | 'destroy' | 'open' | 'close' | 'toggle' | 'sear
|
|||||||
'article' | 'navigate' | 'identify' | 'prefill' | 'reset' | 'logout' | 'config' | 'on' | 'off' |
|
'article' | 'navigate' | 'identify' | 'prefill' | 'reset' | 'logout' | 'config' | 'on' | 'off' |
|
||||||
'once' | 'event' | 'session-data';
|
'once' | 'event' | 'session-data';
|
||||||
|
|
||||||
|
export type BeaconRoute = '/ask/message/' | '/answers/';
|
||||||
|
|
||||||
export interface IUserObj {
|
export interface IUserObj {
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@ -70,6 +72,8 @@ export function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
|
|||||||
(window as any).Beacon(method, options, data);
|
(window as any).Beacon(method, options, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is essentially what's done by the code snippet that HelpScout suggests to install in every
|
||||||
|
// page. In Grist app pages, we only load HelpScout code when the beacon is opened.
|
||||||
function _beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
|
function _beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
|
||||||
_beacon.readyQueue.push({method, options, data});
|
_beacon.readyQueue.push({method, options, data});
|
||||||
}
|
}
|
||||||
@ -103,7 +107,7 @@ let lastOpenType: 'error' | 'message' = 'message';
|
|||||||
* If errors is given, prepares a form for submitting an error report, and includes stack traces
|
* If errors is given, prepares a form for submitting an error report, and includes stack traces
|
||||||
* into the session-data.
|
* into the session-data.
|
||||||
*/
|
*/
|
||||||
function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, errors?: IAppError[]}) {
|
function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) {
|
||||||
const {onOpen, errors} = options;
|
const {onOpen, errors} = options;
|
||||||
|
|
||||||
// The beacon remembers its content, so reset it when switching between reporting errors and
|
// The beacon remembers its content, so reset it when switching between reporting errors and
|
||||||
@ -119,6 +123,16 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro
|
|||||||
if (iframe) { iframe.focus(); }
|
if (iframe) { iframe.focus(); }
|
||||||
if (onOpen) { onOpen(); }
|
if (onOpen) { onOpen(); }
|
||||||
});
|
});
|
||||||
|
Beacon('once', 'article-viewed' as any, () => {
|
||||||
|
// HelpScout creates an iframe with an empty 'src' attribute, then writes to it. In such an
|
||||||
|
// iframe, different browsers interpret relative links differently: Chrome's are relative to
|
||||||
|
// the parent page's URL; Firefox's are relative to the parent page's <base href>.
|
||||||
|
//
|
||||||
|
// Here we set a <base href> explicitly in the iframe to get consistent behavior of links
|
||||||
|
// relative to the top page's URL (HelpScout then seems to handle clicks on them correctly).
|
||||||
|
const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement;
|
||||||
|
iframe?.contentDocument?.head.appendChild(dom('base', {href: ''}));
|
||||||
|
});
|
||||||
Beacon('once', 'close', () => {
|
Beacon('once', 'close', () => {
|
||||||
const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement;
|
const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement;
|
||||||
if (iframe) { iframe.blur(); }
|
if (iframe) { iframe.blur(); }
|
||||||
@ -128,6 +142,7 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attrs: ISessionData = {};
|
const attrs: ISessionData = {};
|
||||||
|
let route: BeaconRoute;
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
// If sending errors, prefill part of the message (the user sees this and can add to it), and
|
// If sending errors, prefill part of the message (the user sees this and can add to it), and
|
||||||
// include more detailed errors with stack traces into session-data.
|
// include more detailed errors with stack traces into session-data.
|
||||||
@ -147,8 +162,10 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro
|
|||||||
attrs[`error-${i}-stack`] = JSON.stringify(error.stack.trim().split('\n'));
|
attrs[`error-${i}-stack`] = JSON.stringify(error.stack.trim().split('\n'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
route = options.route || '/ask/message/';
|
||||||
} else {
|
} else {
|
||||||
Beacon('config', {messaging: {contactForm: {showSubject: true}}});
|
Beacon('config', {messaging: {contactForm: {showSubject: true}}});
|
||||||
|
route = options.route || '/answers/';
|
||||||
}
|
}
|
||||||
|
|
||||||
Beacon('session-data', {
|
Beacon('session-data', {
|
||||||
@ -156,7 +173,7 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro
|
|||||||
...attrs,
|
...attrs,
|
||||||
});
|
});
|
||||||
Beacon('open');
|
Beacon('open');
|
||||||
Beacon('navigate', '/ask/message/');
|
Beacon('navigate', route);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBeaconOpenOptions {
|
export interface IBeaconOpenOptions {
|
||||||
@ -164,6 +181,7 @@ export interface IBeaconOpenOptions {
|
|||||||
includeAppErrors?: boolean;
|
includeAppErrors?: boolean;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
errors?: IAppError[];
|
errors?: IAppError[];
|
||||||
|
route?: BeaconRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,23 +25,22 @@ import {dom, DomContents, Observable, styled} from 'grainjs';
|
|||||||
* HelpCenter in a new tab.
|
* HelpCenter in a new tab.
|
||||||
*/
|
*/
|
||||||
export function createHelpTools(appModel: AppModel, spacer = true): DomContents {
|
export function createHelpTools(appModel: AppModel, spacer = true): DomContents {
|
||||||
const isEfcr = (appModel.topAppModel.productFlavor === 'efcr');
|
|
||||||
return [
|
return [
|
||||||
spacer ? cssSpacer() : null,
|
spacer ? cssSpacer() : null,
|
||||||
cssPageEntry(
|
cssSplitPageEntry(
|
||||||
cssPageLink(cssPageIcon('Feedback'),
|
cssPageEntryMain(
|
||||||
cssLinkText('Give Feedback'),
|
cssPageLink(cssPageIcon('Help'),
|
||||||
dom.on('click', () => beaconOpenMessage({appModel})),
|
cssLinkText('Help Center'),
|
||||||
|
dom.cls('tour-help-center'),
|
||||||
|
dom.on('click', (ev) => beaconOpenMessage({appModel})),
|
||||||
|
testId('left-feedback'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
dom.hide(isEfcr),
|
cssPageEntrySmall(
|
||||||
testId('left-feedback'),
|
cssPageLink(cssPageIcon('FieldLink'),
|
||||||
),
|
{href: commonUrls.help, target: '_blank'},
|
||||||
cssPageEntry(
|
),
|
||||||
cssPageLink(cssPageIcon('Help'), {href: commonUrls.help, target: '_blank'},
|
)
|
||||||
cssLinkText('Help Center'),
|
|
||||||
dom.cls('tour-help-center')
|
|
||||||
),
|
|
||||||
dom.hide(isEfcr),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -156,3 +155,28 @@ export const cssPageIcon = styled(icon, `
|
|||||||
export const cssSpacer = styled('div', `
|
export const cssSpacer = styled('div', `
|
||||||
height: 18px;
|
height: 18px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssSplitPageEntry = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssPageEntryMain = styled(cssPageEntry, `
|
||||||
|
flex: auto;
|
||||||
|
margin: 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssPageEntrySmall = styled(cssPageEntry, `
|
||||||
|
flex: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
--icon-color: ${colors.lightGreen};
|
||||||
|
& > .${cssPageLink.className} {
|
||||||
|
padding: 0 8px 0 16px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
--icon-color: ${colors.darkGreen};
|
||||||
|
}
|
||||||
|
.${cssTools.className}-collapsed & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -145,7 +145,7 @@ function buildNotifyDropdown(ctl: IOpenController, notifier: Notifier, appModel:
|
|||||||
cssDropdownFeedbackLink(
|
cssDropdownFeedbackLink(
|
||||||
cssDropdownFeedbackIcon('Feedback'),
|
cssDropdownFeedbackIcon('Feedback'),
|
||||||
'Give feedback',
|
'Give feedback',
|
||||||
dom.on('click', () => beaconOpenMessage({appModel, onOpen: () => ctl.close()})),
|
dom.on('click', () => beaconOpenMessage({appModel, onOpen: () => ctl.close(), route: '/ask/message/'})),
|
||||||
testId('feedback'),
|
testId('feedback'),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -39,7 +39,7 @@ export function getOrgFromRequest(req: Request): string|null {
|
|||||||
* HelpScout the user identity for identifying customer information and conversation history.
|
* HelpScout the user identity for identifying customer information and conversation history.
|
||||||
*/
|
*/
|
||||||
function helpScoutSign(email: string): string|undefined {
|
function helpScoutSign(email: string): string|undefined {
|
||||||
const secretKey = process.env.HELP_SCOUT_SECRET_KEY;
|
const secretKey = process.env.HELP_SCOUT_SECRET_KEY_V2;
|
||||||
if (!secretKey) { return undefined; }
|
if (!secretKey) { return undefined; }
|
||||||
return crypto.createHmac('sha256', secretKey).update(email).digest('hex');
|
return crypto.createHmac('sha256', secretKey).update(email).digest('hex');
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial<GristLoadCo
|
|||||||
stripeAPIKey: process.env.STRIPE_PUBLIC_API_KEY,
|
stripeAPIKey: process.env.STRIPE_PUBLIC_API_KEY,
|
||||||
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
||||||
googleDriveScope: process.env.GOOGLE_DRIVE_SCOPE,
|
googleDriveScope: process.env.GOOGLE_DRIVE_SCOPE,
|
||||||
helpScoutBeaconId: process.env.HELP_SCOUT_BEACON_ID,
|
helpScoutBeaconId: process.env.HELP_SCOUT_BEACON_ID_V2,
|
||||||
maxUploadSizeImport: (Number(process.env.GRIST_MAX_UPLOAD_IMPORT_MB) * 1024 * 1024) || undefined,
|
maxUploadSizeImport: (Number(process.env.GRIST_MAX_UPLOAD_IMPORT_MB) * 1024 * 1024) || undefined,
|
||||||
maxUploadSizeAttachment: (Number(process.env.GRIST_MAX_UPLOAD_ATTACHMENT_MB) * 1024 * 1024) || undefined,
|
maxUploadSizeAttachment: (Number(process.env.GRIST_MAX_UPLOAD_ATTACHMENT_MB) * 1024 * 1024) || undefined,
|
||||||
timestampMs: Date.now(),
|
timestampMs: Date.now(),
|
||||||
|
Loading…
Reference in New Issue
Block a user