mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Reopen HelpScout beacon to the last-opened article
Summary: - When opening HelpScout beacon to an article ("answers"), avoid a 'navigate' call to let the beacon show the previously open article. - Work around a bug with reloading a page with a beacon article open: HelpScout renders the last state without triggering usual events. - Report errors to server when beacon fails to load. - reportWarning() method now reports the message to the server. Test Plan: Added a test case Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3345
This commit is contained in:
parent
499e24b744
commit
d55bdbcdf3
@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
// tslint:disable:unified-signatures
|
// tslint:disable:unified-signatures
|
||||||
|
|
||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
|
import {reportWarning} from 'app/client/models/errors';
|
||||||
import {IAppError} from 'app/client/models/NotifyModel';
|
import {IAppError} from 'app/client/models/NotifyModel';
|
||||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||||
import {timeFormat} from 'app/common/timeFormat';
|
import {timeFormat} from 'app/common/timeFormat';
|
||||||
@ -64,7 +65,8 @@ export function Beacon(method: 'navigate', route: string): void;
|
|||||||
export function Beacon(method: 'identify', userObj: IUserObj): void;
|
export function Beacon(method: 'identify', userObj: IUserObj): void;
|
||||||
export function Beacon(method: 'prefill', formObj: IFormObj): void;
|
export function Beacon(method: 'prefill', formObj: IFormObj): void;
|
||||||
export function Beacon(method: 'config', configObj: object): void;
|
export function Beacon(method: 'config', configObj: object): void;
|
||||||
export function Beacon(method: 'on'|'off'|'once', event: 'open'|'close', callback: () => void): void;
|
export function Beacon(method: 'on'|'once', event: string, callback: () => void): void;
|
||||||
|
export function Beacon(method: 'off', event: string, callback?: () => void): void;
|
||||||
export function Beacon(method: 'session-data', data: ISessionData): void;
|
export function Beacon(method: 'session-data', data: ISessionData): void;
|
||||||
export function Beacon(method: BeaconCmd): void;
|
export function Beacon(method: BeaconCmd): void;
|
||||||
export function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
|
export function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
|
||||||
@ -85,21 +87,30 @@ function initBeacon(): void {
|
|||||||
const beaconId = gristConfig && gristConfig.helpScoutBeaconId;
|
const beaconId = gristConfig && gristConfig.helpScoutBeaconId;
|
||||||
if (beaconId) {
|
if (beaconId) {
|
||||||
(window as any).Beacon = _beacon;
|
(window as any).Beacon = _beacon;
|
||||||
document.head.appendChild(dom('script', {
|
document.head.appendChild(dom('script',
|
||||||
type: 'text/javascript',
|
{
|
||||||
src: 'https://beacon-v2.helpscout.net',
|
type: 'text/javascript',
|
||||||
async: true,
|
src: 'https://beacon-v2.helpscout.net',
|
||||||
}));
|
async: true,
|
||||||
|
},
|
||||||
|
// Report when the beacon fails to load so that the user knows something is wrong, and we
|
||||||
|
// have a log of the error. (Note: might not report all failures due to ad-blockers.)
|
||||||
|
dom.on('error', (e) => {
|
||||||
|
reportWarning("Support form failed to load. " +
|
||||||
|
"Please email support@getgrist.com with questions instead.");
|
||||||
|
}),
|
||||||
|
));
|
||||||
_beacon('init', beaconId);
|
_beacon('init', beaconId);
|
||||||
_beacon('config', {display: {style: "manual"}});
|
_beacon('config', {display: {style: "manual"}});
|
||||||
} else {
|
} else {
|
||||||
(window as any).Beacon = () => null;
|
(window as any).Beacon = () => null;
|
||||||
reportError(new Error("Support form is not configured"));
|
reportWarning("Support form is not configured");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastOpenType: 'error' | 'message' = 'message';
|
let lastOpenType: 'error' | 'message' = 'message';
|
||||||
|
let lastRoute: BeaconRoute|null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to open a beacon, taking care of setting focus appropriately. Calls optional onOpen
|
* Helper to open a beacon, taking care of setting focus appropriately. Calls optional onOpen
|
||||||
@ -118,31 +129,33 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) {
|
|||||||
lastOpenType = openType;
|
lastOpenType = openType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const route: BeaconRoute = options.route || (errors?.length ? '/ask/message/' : '/answers/');
|
||||||
|
// If beacon was and still is being opened for help articles, avoid the 'navigate' call
|
||||||
|
// altogether, to keep the beacon at the last article it was on.
|
||||||
|
const skipNav = (route === lastRoute && route === '/answers/');
|
||||||
|
lastRoute = route;
|
||||||
|
|
||||||
Beacon('once', 'open', () => {
|
Beacon('once', 'open', () => {
|
||||||
const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement;
|
const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement;
|
||||||
if (iframe) { iframe.focus(); }
|
if (iframe) { iframe.focus(); }
|
||||||
if (onOpen) { onOpen(); }
|
if (onOpen) { onOpen(); }
|
||||||
});
|
});
|
||||||
Beacon('once', 'article-viewed' as any, () => {
|
// Fix base-href tag when opening an article.
|
||||||
// HelpScout creates an iframe with an empty 'src' attribute, then writes to it. In such an
|
Beacon('once', 'article-viewed', () => fixBeaconBaseHref());
|
||||||
// iframe, different browsers interpret relative links differently: Chrome's are relative to
|
// We duplicate this check for 'ready' event, because 'open' and 'article-viewed' events don't
|
||||||
// the parent page's URL; Firefox's are relative to the parent page's <base href>.
|
// trigger on page reload when a beacon article is already open (seems to be a HelpScout bug).
|
||||||
//
|
Beacon('once', 'ready', () => fixBeaconBaseHref());
|
||||||
// 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(); }
|
||||||
|
Beacon('off', 'article-viewed');
|
||||||
});
|
});
|
||||||
if (userObj) {
|
if (userObj) {
|
||||||
Beacon('identify', userObj);
|
Beacon('identify', userObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
@ -162,10 +175,8 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) {
|
|||||||
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', {
|
||||||
@ -173,7 +184,23 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) {
|
|||||||
...attrs,
|
...attrs,
|
||||||
});
|
});
|
||||||
Beacon('open');
|
Beacon('open');
|
||||||
Beacon('navigate', route);
|
if (!skipNav) {
|
||||||
|
Beacon('navigate', route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixBeaconBaseHref() {
|
||||||
|
// 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;
|
||||||
|
const iframeDoc = iframe?.contentDocument;
|
||||||
|
if (iframeDoc && !iframeDoc.querySelector('head > base')) {
|
||||||
|
iframeDoc.head.appendChild(dom('base', {href: ''}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBeaconOpenOptions {
|
export interface IBeaconOpenOptions {
|
||||||
|
@ -53,10 +53,14 @@ export function reportMessage(msg: string, options?: Partial<INotifyOptions>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows warning toast notification (with yellow styling).
|
* Shows warning toast notification (with yellow styling), and log to server and to console. Pass
|
||||||
|
* {level: 'error'} for same behavior with adjusted styling.
|
||||||
*/
|
*/
|
||||||
export function reportWarning(msg: string, options?: Partial<INotifyOptions>) {
|
export function reportWarning(msg: string, options?: Partial<INotifyOptions>) {
|
||||||
reportMessage(msg, {level: 'warning', ...options});
|
options = {level: 'warning', ...options};
|
||||||
|
log.warn(`${options.level}: `, msg);
|
||||||
|
_logError(msg);
|
||||||
|
reportMessage(msg, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user